diff --git a/README.md b/README.md index f775de1..02fa934 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ version 3.8 or higher (3.7 sometimes crashes during compilation). The datasheet of the STM8S005 claims a write endurance of only 100 flash cycles but this is only for marketing purposes as it [contains the same die](https://hackaday.io/project/16097-eforth-for-cheap-stm8s-gadgets/log/76731-stm8l001j3-a-new-sop8-chip-and-the-limits-of-stm8flash) as the STM8S105 which is rated for 10000 cycles. So you don't have to worry -about bricking your device by flashing it too often. Mine has far more than 100 +about bricking you device by flashing it to often. Mine has far more than 100 cycles and still works. You can easily verify the two chips are the same as you can write the whole 1kB of EEPROM the STM8S105 has instead of only 128 bytes as claimed by the STM8S005 datasheet. @@ -103,6 +103,58 @@ run mode. * SUP: 12V input voltage is too low. Connect better power supply. * INT: Internal error. Should not happen. Check the source code where this error is set and try to fix it. +### Serial Remote Control +Command can be sent to the device over the serial port. All settings can be modified and the load can be enabled and disabled. A help system is included. Send `HELP` or `command HELP` for details. The code is designed to be easily expandable if you need to add more commands. + +#### SETMODE [CC|CV|CR|CP] +Set the operating Mode of the electronic load. + * CC = Constant Current + * CV = Constant Voltage + * CR = Constant Resistance + * CP = Constant Power + +#### SETI [mA] +Set the target amperage for constant current mode. + * mA = Target amperage in milliamps between 200 (0.2A) and 10000 (10A) + +#### SETV [mV] +Set the target voltage for constant voltage mode. + * mV = Target voltage in millivolts between 500 (0.5V) and 30000 (30V). + +#### SETR [mR] +Set the target resistance for constant resistance mode. + * mR = Target resistance in milliohms between 10 (0.01 ohms) and 15000 (15 ohms)\n" + +#### SETP [mW] +Set the target power for constant power mode. + * mW = Target power in milliwatts between 0 and 60000 (60W) + +#### SETBEEP [OFF|ON] +Enable or disable alarm and notification sounds. + * OFF = Turn off alarms and notifications + * ON = Turn on alarms and notifications + +#### SETCUTOFF [mV|OFF] +Target cutoff voltage. If the voltage falls below target, the load will be disabled. + * mV = Cutoff voltage in millivolts between 500 (0.5V) and 30000 (30V) + * OFF = No action will be taken if voltage falls too low + +#### SETILIMIT [mA] +Set current limit. If this current is exceeded, the load will be disabled. + * mA = Amperage limit in milliamps between 200 (0.2A) and 10000 (10A) + +#### SETPLIMIT [NOLIMIT|LIMIT] +Set action to take if maximum power limit is exceeded. + * NOLIMIT = No action is taken + * LIMIT = Load will be disabled if power limit is exceeded + +#### LOAD [ON|OFF] +Requests the load to activate or deactivate. + * ON = Activates the load + * OFF = Deactivates the load +NOTE: ON is similar to pressing the run button and will take you up one menu level. It will only enable the load if you are at the top menu level. Send multiple LOAD ON commands until you see the realtime output indicate the load is on. +NOTE: OFF disables the load but does not return you to the menu. Use the hardware run button if you need to get in to the menu + ## History This firmware started as a project to extend the firmware written by [soundstorm](https://github.com/ArduinoHannover/ZPB30A1_Firmware) but it turned diff --git a/serial protocol.md b/serial protocol.md deleted file mode 100644 index 8b3046c..0000000 --- a/serial protocol.md +++ /dev/null @@ -1,55 +0,0 @@ -# Serial protocol - -## Pins -Left side of the base PCB: -* G: GND -* R: RXD -* T: TXD -* Vc: +12V -* Others: Unused - -## Settings -115200 Baud, 8N1 - -## Value readback -The device continously outputs it current state. - -Example: `VAL:D 0 T 248 Vi 11813 Vl 101 Vs 0 I 2500 mWs 0 mAs 0` - -Each line contains the following fields: -* Message type marker: Always "VAL:" -* Device state: - * 'D': Disabled - * 'A': Active and in regulation - * 'U': Out of regulation, i.e. source can't supply enough power. As this load does not measure the current the reported current will be wrong when the load is out of regulation! -* Error (no column name): Single digit integer (see load.h for error codes) -* T: Temperature in 0.1°C -* Vi: Power supply voltage (12V nom.) in mV -* Vl: Load voltage (screw terminals) in mV -* Vs: Sense voltage (plugable connector) in mV -* I: Current in mA. As this load does not measure the current the setpoint is reported. -* mWs: Energy since start of measurement (in mWs) -* mAs: Energy since start of measurement (in mAs) - -## Configuration -Configuration protocol currently is quite simple. There are two command formats: -* character\r\n: Execute a command without parameters -* characterINTEGER\r\n: Set a parameter. Integer value must fit in 16 bits - -The command is executed when the \n is received. The \r is optional. - -Commands: -* !: Reset UART state. Must be sent after establishing a connection or after receiving an error reply. -* R: Run -* S: Stop -* M: Mode (0=CC, 1=CW, 2=CR, 3=CV, see settings.h) -* c: Setpoint CC in mA -* w: Setpoint CW in mW -* r: Setpoint CR in 0.1 Ohm -* v: Setpoint CV in mV -* E: Write settings to EEPROM. Only when settings are changed via the UI they are automatically written to EEPROM. Settings via the serial interface must be written using this command explicitly. However when the user changes any setting via the UI ALL settings are written to EEPROM. -* e: Read settings from EEPROM. This should be used after controlling the device via the serial interface to restore user's settings. - -Once a command is executed the device replies with: `CMD:[Received command]`. Received command is not necessarily exactly the same string that was sent to the device but the parsed interpretation. For example the response to `c01234` is `CMD:c1234`. - -If the command is invalid an error line is produced. Example: `ERR:97 0 1` First parameter is the ASCII code of the received command, second parameter is the received parameter and third parameter the error code (defined in uart.h). After each error the interface should be reset. Depending on the type of error (parsing error vs. parameter error) more than one error message might be returned per command. Sending `Hello World` will probably return one error for each of the characters after the first one as they are all invalid. But don't count on this behavior as the interface might be to slow to output all messages and discard some of them. diff --git a/software/Makefile b/software/Makefile index 2f8a0ae..2176c90 100644 --- a/software/Makefile +++ b/software/Makefile @@ -7,7 +7,7 @@ STLINK_VERSION=2 MAIN=electronic_load.c SRC=tm1650.c uart.c utils.c fan.c ui.c eeprom.c timer.c load.c settings.c \ - adc.c beeper.c menu_items.c + adc.c beeper.c menu_items.c remote.c BUILDDIR=build SRC:=$(MAIN) $(SRC) diff --git a/software/README.md b/software/README.md index 270fb22..7082bbd 100644 --- a/software/README.md +++ b/software/README.md @@ -1,3 +1,176 @@ +# ZPB30A1 Firmware +This repository is there to build an open-source firmware for the 60W ZPB30A1 electronic load (often sold as "60W electronic load" without any article number). + +## Features +* Modes + * CC: Constant current + * CR: Constant resistance + * CP: Constant power + * CV: Constant voltage (slow regulation due to hardware limitations) +* Easily usable menu system with many configuration options +* Logging of all operating parameters via UART. +* Much better accuracy than stock firmware. +* Clean firmware structure for easy extendability. + + +## Original Firmware +The original firmware has read out protection enabled. Therefore you can't go +back to this version once you flash a different one. But this firmware aims to +be much better in every possible way. If you need a feature either add it on +your own and submit a pull request or open an issue and wait till I add it. + +## Compiler + +This software requires [Small Device C Compiler (SDCC)](http://sdcc.sourceforge.net/) +version 3.8 or higher (3.7 sometimes crashes during compilation). + +## Chip +The datasheet of the STM8S005 claims a write endurance of only 100 flash cycles +but this is only for marketing purposes as it [contains the same die](https://hackaday.io/project/16097-eforth-for-cheap-stm8s-gadgets/log/76731-stm8l001j3-a-new-sop8-chip-and-the-limits-of-stm8flash) +as the STM8S105 which is rated for 10000 cycles. So you don't have to worry +about bricking you device by flashing it to often. Mine has far more than 100 +cycles and still works. You can easily verify the two chips are the same as you +can write the whole 1kB of EEPROM the STM8S105 has instead of only 128 bytes +as claimed by the STM8S005 datasheet. + + +## Flashing +As the original firmware has the bootloader disabled you need a STLink programmer +in order to unlock it. Connect it like this: + +![Programmer connection](images/stlink.jpg) + +If you are programming the chip for the first time you have to unlock it. +'''WARNING:''' This irreversibly deletes the original firmware. + + make unlock + +Then you write the new firmware with + + make flash + +and if you are flashing for the first time you should also clear the EEPROM: + + make clear_eeprom + +## Menu +Contrary to the original firmware which requires rebooting to change modes this +firmware is completely configurable by an integrated menu system. Push the +encoder button to select an item. The "Run" button acts as a back button in most +situations. Only in the top level menu it is used to enable the electronic load. +The currently selected option blinks. When setting a numeric value the two +LEDs between the displays show the selected digit. +Values shown with a decimal point are in the unit shown by the LEDs or on the +display. If no decimal point is shown it means the display shows 1/1000 of the +selected unit. +### Examples +* 10.0 + V LED: 10.0V +* 1.23 + A LED: 1.23A +* 900 + V LED: 900mV = 0.9V + + +### Menu structure +* MODE + * CC: Constant current (default) + * CV: Constant voltage + * CR: Constant resistance + * CW: Constant power +* VAL: Sets the target value for the currently selected mode. The upper display + shows the unit. +* ILIM: Current limit (not active in CC mode) +* ...: More settings + * BEEP: Beeper on/off + * CUTO: Undervoltage cutoff + * ENAB: Enable/disable + * CVAL: Cutoff value in Volt + * MAXP: Maximum power action + * OFF: Turn off load when the required power would be greater than the hardware limit + * LIM: Reduce load current to stay within hardware limits + +## Run mode +While in run mode the top display show V, Ah, or Wh. The bottom display show +the current. +The unit in the top display switches automatically after some seconds. Rotating the encoder +changes the unit manually and disables automatic switching. +Pressing the encoder enters a menu to change the current setpoint without exiting +run mode. + +### Error codes +* OVP: Over voltage protection. Voltage connected to P+/P- is too high. (Note: This function can only warn about voltages which are slightly to high. Large voltages will destroy the electronic load!) +* OVLD: The load can't maintain the set value. Usually this means that the source can't deliver enough current or the source's voltage is to low. +* PWR: Power required to maintain the setpoint is greater than hardware's power limit. +* TEMP: Temperature is to high. Check if the fan is working and the thermistor is connected. +* SUP: 12V input voltage is too low. Connect better power supply. +* INT: Internal error. Should not happen. Check the source code where this error is set and try to fix it. + +### Serial Remote Control +Command can be sent to the device over the serial port. All settings are can be modified and the load can be enabled and disabled. A help system is included. The code is designed to be easily expandable if you need to add more commands. + +* SETMODE [CC|CV|CR|CP] +Set the operating Mode of the electronic load. + CC = Constant Current + CV = Constant Voltage + CR = Constant Resistance + CP = Constant Power + +* SETI [mA] +Set the target amperage for constant current mode. + mA = Target amperage in milliamps between 200 (0.2A) and 10000 (10A) + +* SETV [mV] +Set the target voltage for constant voltage mode. + mV = Target voltage in millivolts between 500 (0.5V) and 30000 (30V). + +* SETR [mR] +Set the target resistance for constant resistance mode. + mR = Target resistance in milliohms between 10 (0.01 ohms) and 15000 (15 ohms)\n" + +* SETP [mW] +Set the target power for constant power mode. + mW = Target power in milliwatts between 0 and 60000 (60W) + +* SETBEEP [OFF|ON] +Enable or disable alarm and notification sounds. + OFF = Turn off alarms and notifications + ON = Turn on alarms and notifications + +* SETCUTOFF [mV|OFF] +Target cutoff voltage. If the voltage falls below target, the load will be disabled. + mV = Cutoff voltage in millivolts between 500 (0.5V) and 30000 (30V) + OFF = No action will be taken if voltage falls too low + +* SETILIMIT [mA] +Set current limit. If this current is exceeded, the load will be disabled. + mA = Amperage limit in milliamps between 200 (0.2A) and 10000 (10A) + +* SETPLIMIT [NOLIMIT|LIMIT] +Set action to take if maximum power limit is exceeded. + NOLIMIT = No action is taken + LIMIT = Load will be disabled if power limit is exceeded + +* LOAD [ON|OFF] +Requests the load to activate or deactivate. + ON = Activates the load + OFF = Deactivates the load +NOTE: ON is similar to pressing the run button and will take you up one menu level. It will only enable the load if you are at the top menu level. Send multiple LOAD ON commands until you see the realtime output indicate the load is on. +NOTE: OFF disables the load but does not return you to the menu. Use the hardware run button if you need to get in to the menu + +## History +This firmware started as a project to extend the firmware written by +[soundstorm](https://github.com/ArduinoHannover/ZPB30A1_Firmware) but it turned +out as an almost complete rewrite which keeps the user interface idea. + +## Schematic +Schematic can be found in [hardware/schematic.pdf]. + +### Schematic corrections: +* R5 = 10k +* R27 = 1k +* D6 = reversed polarity + +### Component locations +![Component locations](images/components.jpg) + # Module interface * NAME_init(): Initialize hardware and variables. Set module to a safe state * NAME_timer(): Called when the systick timer counts from the main loop. @@ -8,3 +181,4 @@ * TIM2: Systick * TIM3: CCR2: Fan * TIM4: + diff --git a/software/electronic_load.c b/software/electronic_load.c index 1f74cf1..d69c8b4 100644 --- a/software/electronic_load.c +++ b/software/electronic_load.c @@ -11,6 +11,7 @@ #include "fan.h" #include "adc.h" #include "beeper.h" +#include "remote.h" #include "inc/stm8s_clk.h" #include "inc/stm8s_exti.h" #include "inc/stm8s_itc.h" @@ -55,10 +56,12 @@ void gpio_init() void main(void) { + clock_init(); gpio_init(); adc_init(); uart_init(); + remote_init(); systick_init(); load_init(); beeper_init(); @@ -66,15 +69,15 @@ void main(void) { settings_init(); __asm__ ("rim"); - + // Power on beep beeper_on(); delay10ms(10); beeper_off(); - + // Init UI after power on delay to avoid spurious button events. ui_init(); - + systick_flag = 0; // Clear any overflows up to this point while (1) { if (systick_flag & SYSTICK_OVERFLOW) @@ -88,10 +91,11 @@ void main(void) { ui_timer(); load_timer(); uart_timer(); + remote_timer(); systick_flag &= ~SYSTICK_COUNT; } - uart_handler(); } + } //Voltage OK interrupt diff --git a/software/remote.c b/software/remote.c new file mode 100644 index 0000000..c758212 --- /dev/null +++ b/software/remote.c @@ -0,0 +1,389 @@ +/* +Remote control over serial +AUTHOR: Tom Haynes (code@tomhaynes.net) +DATE: September 11th, 2020 +*/ +#include "remote.h" +#include "uart.h" +#include "settings.h" +#include "config.h" +#include "load.h" +#include "timer.h" +#include "ui.h" +#include "menu_items.h" +#include +#include +#include +#include + +typedef enum { + WAITING_FOR_COMMAND, + PROCESSING_COMMAND, + DEBUG +} receive_state_t; + +typedef struct Command_t Command; +typedef void (*remote_callback_func)(uint8_t *data, Command *cmd); + +struct Command_t { + const uint8_t *name; + const remote_callback_func handler; + const uint8_t *help; +}; + + + +// To add a new command, extend the Commands[] array below. Then add a new +// handler function in the callback section. Most commands will probably update +// the settings object so check out settings.h for details. + +//Define new commands here +static const Command Commands[] = { + //SETMODE [CC|CV|CR|CP] + { .name = "SETMODE", + .handler = &remote_setmode, + .help = " SETMODE [CC|CV|CR|CP]\n" + " Set the operating Mode of the electronic load.\n" + " CC = Constant Current\n" + " CV = Constant Voltage\n" + " CR = Constant Resistance\n" + " CP = Constant Power\n" + }, + //SETI [amperage] #mA 200(CUR_MIN)-10000(CUR_MAX) + { .name = "SETI", + .handler = &remote_seti, + .help = " SETI [mA]\n" + " Set the target amperage for constant current mode.\n" + //TODO: This should be calculated based on defined constants + " mA = Target amperage in milliamps between 200 (0.2A) and 10000 (10A)\n" + }, + //SETV [voltage] #mV 500(VOLT_MIN)-30000(VOLT_MAX) + { .name = "SETV", + .handler = &remote_setv, + .help = " SETV [mV]\n" + " Set the target voltage for constant voltage mode.\n" + //TODO: This should be calculated based on defined constants + " mV = Target voltage in millivolts between 500 (0.5V) and 30000 (30V)\n" + }, + //SETR [resistance] #mR 10(R_MIN)-15000(R_MAX) + { .name = "SETR", + .handler = &remote_setr, + .help = " SETR [mR]\n" + " Set the target resistance for constant resistance mode.\n" + //TODO: This should be calculated based on defined constants + " mR = Target resistance in milliohms between 10 (0.01 ohms) and 15000 (15 ohms)\n" + }, + //SETP [power] #mW 0(POW_MIN)-60000(POW_MAX) + { .name = "SETP", + .handler = &remote_setp, + .help = " SETP [mW]\n" + " Set the target power for constant power mode.\n" + //TODO: This should be calculated based on defined constants + " mW = Target power in milliwatts between 0 and 60000 (60W)\n" + }, + //SETBEEP [OFF|ON] + { .name = "SETBEEP", + .handler = &remote_setbeep, + .help = " SETBEEP [OFF|ON]\n" + " Enable or disable alarm and notification sounds.\n" + " OFF = Turn off alarms and notifications\n" + " ON = Turn on alarms and notifications\n" + }, + //SETCUTOFF [voltage|OFF] #mV 500(VOLT_MIN)-30000(VOLT_MAX) + { .name = "SETCUTOFF", + .handler = &remote_setcutoff, + .help = " SETCUTOFF [mV|OFF]\n" + " Target cutoff voltage. If the voltage falls below target, the load will be disabled.\n" + //TODO: This should be calculated based on defined constants + " mV = Cutoff voltage in millivolts between 500 (0.5V) and 30000 (30V)\n" + " OFF = No action will be taken if voltage falls too low\n" + }, + //SETILIMIT [current] #mA 200(CUR_MIN)-10000(CUR_MAX) + { .name = "SETILIMIT", + .handler = &remote_setilimit, + .help = " SETILIMIT [mA]\n" + " Set current limit. If this current is exceeded, the load will be disabled.\n" + //TODO: This should be calculated based on defined constants + " mA = Amperage limit in milliamps between 200 (0.2A) and 10000 (10A)\n" + }, + //SETPLIMIT [NOLIMIT|LIMIT] + { .name = "SETPLIMIT", + .handler = &remote_setplimit, + .help = " SETPLIMIT [NOLIMIT|LIMIT]\n" + " Set action to take if maximum power limit is exceeded.\n" + " NOLIMIT = No action is taken\n" + " LIMIT = Load will be disabled if power limit is exceeded\n" + }, + //LOAD [ON|OFF] + { .name = "LOAD", + .handler = &remote_load, + .help = " LOAD [ON|OFF]\n" + " Requests the load to activate or deactivate.\n" + " ON = Activates the load\n" + " OFF = Deactivates the load\n" + " NOTE: ON is similar to pressing the run button and will take\n" + " you up one menu level. It will only enable the load if you are\n" + " at the top menu level. Send multiple LOAD ON commands until you\n" + " see the realtime output indicate the load is on.\n" + " NOTE: OFF disables the load but does not return you to the menu.\n" + " Use the hardware run button if you need to get in to the menu\n" + } +}; + +// Define new callback functions here +//SETMODE [CC|CV|CR|CP] +void remote_setmode(uint8_t *data, Command *cmd) { + if (strcmp(data, "CC") == 0) { + settings.mode = MODE_CC; + printf("Mode set to constant current\n"); + } else if (strcmp(data, "CV") == 0) { + settings.mode = MODE_CV; + printf("Mode set to constant voltage\n"); + } else if (strcmp(data, "CR") == 0) { + settings.mode = MODE_CR; + printf("Mode set to constant resistance\n"); + } else if (strcmp(data, "CP") == 0) { + settings.mode = MODE_CW; + printf("Mode set to constant power\n"); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + } +} + +//SETI [amperage] #mA 200(CUR_MIN)-10000(CUR_MAX) +void remote_seti (uint8_t *data, Command *cmd) { + uint16_t value = atoi(data); + if (value >= CUR_MIN && value <= CUR_MAX) { + settings.setpoints[MODE_CC] = value; + printf("Target current set to %d mA\n", value); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + printf("Must be %d <= value <= %d\n", CUR_MIN, CUR_MAX); + } +} + +//SETV [voltage] #mV 500(VOLT_MIN)-30000(VOLT_MAX) +void remote_setv (uint8_t *data, Command *cmd) { + uint16_t value = atoi(data); + if (value >= VOLT_MIN && value <= VOLT_MAX) { + settings.setpoints[MODE_CV] = value; + printf("Target voltage set to %d mV\n", value); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + printf("Must be %d <= value <= %d\n", VOLT_MIN, VOLT_MAX); + } +} + +//SETR [resistance] #mR 10(R_MIN)-15000(R_MAX) +void remote_setr (uint8_t *data, Command *cmd) { + uint16_t value = atoi(data); + if (value >= R_MIN && value <= R_MAX) { + settings.setpoints[MODE_CR] = value; + printf("Target resistance set to %d mOhm\n", value); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + printf("Must be %d <= value <= %d\n", R_MIN, R_MAX); + } +} + +//SETP [power] #mW 0(POW_MIN)-60000(POW_MAX) +//NOTE: value >= POW_MIN throws a compile time warning when POW_MIN = 0 +//because value is an unsigned integer so it will always be >= 0. this +//can safely be ignored. +#pragma save +#pragma disable_warning 94 +void remote_setp (uint8_t *data, Command *cmd) { + uint16_t value = atoi(data); + if (value == 0 && strcmp(data, "0") != 0) { + // 0 may a valid value but atoi also returns 0 if it doesn't see a number. + // If the string value of data is not "0" then the int value of value + // means there was an error in the atoi conversion. + printf("ERROR Invalid command data \"%s\"\n", data); + printf("Must be %d <= value <= %d\n", CUR_MIN, CUR_MAX); + } else if (value >= POW_MIN && value <= POW_MAX) { + settings.setpoints[MODE_CW] = value; + printf("Target power set to %d mW\n", value); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + printf("Must be %d <= value <= %d\n", CUR_MIN, CUR_MAX); + } +} +#pragma restore + +//SETBEEP [OFF|ON] +void remote_setbeep (uint8_t *data, Command *cmd) { + if (strcmp(data, "ON") == 0) { + settings.beeper_enabled = true; + printf("Beeper enabled\n"); + } else if (strcmp(data, "OFF") == 0) { + settings.beeper_enabled = false; + printf("Beeper disabled\n"); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + } +} + +//SETCUTOFF [voltage|OFF] #mV 500(VOLT_MIN)-30000(VOLT_MAX) +void remote_setcutoff (uint8_t *data, Command *cmd) { + uint16_t value = atoi(data); + if(strcmp(data, "OFF") == 0) { + settings.cutoff_enabled = false; + printf("Voltage cutoff disabled\n"); + } else if (value >= VOLT_MIN && value <= VOLT_MAX) { + settings.cutoff_voltage = value; + settings.cutoff_enabled = true; + printf("Voltage cutoff set to %d mV and enabled\n", value); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + printf("Must be %d <= value <= %d\n", VOLT_MIN, VOLT_MAX); + } +} + +//SETILIMIT [current] #mA 200(CUR_MIN)-10000(CUR_MAX) +void remote_setilimit (uint8_t *data, Command *cmd) { + uint16_t value = atoi(data); + if (value >= CUR_MIN && value <= CUR_MAX) { + settings.current_limit = value; + printf("Current limit set to %d mA\n", value); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + printf("Must be %d <= value <= %d\n", CUR_MIN, CUR_MAX); + } +} + +//SETPLIMIT [NOLIMIT|LIMIT] +void remote_setplimit (uint8_t *data, Command *cmd) { + if (strcmp(data, "LIMIT") == 0) { + settings.max_power_action = MAX_P_LIM; + printf("Maximum power limit enabled\n"); + } else if (strcmp(data, "NOLIMIT") == 0) { + settings.max_power_action = MAX_P_OFF; + printf("Maximum power limit disabled\n"); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + } +} + +//LOAD [ON|OFF] +void remote_load (uint8_t *data, Command *cmd) { + (void) cmd; //unused + uint16_t value = atoi(data); + if (strcmp(data, "ON") == 0) { + //load_enable(); + ui_submenu(EVENT_RUN_BUTTON, NULL); + printf("Load enabled\n"); + } else if (strcmp(data, "OFF") == 0) { + load_disable(DISABLE_USER); + printf("Load disabled\n"); + } else if (strcmp(data, "HELP") == 0) { + printf("%s\n", cmd->help); + } else { + printf("ERROR Invalid command data \"%s\"\n", data); + } +} + +#define NUMBER_OF_COMMANDS (sizeof(Commands) / sizeof(Command)) +// TODO: Set automatically based on command definitions above +#define MAX_COMMAND_LENGTH 10 +// TODO: Set automatically based on command definitions above +#define MIN_COMMAND_LENGTH 4 +// TODO: Set automatically based on command definitions above +#define MAX_DATA_LENGTH 7 +#define COMMAND_BUFFER_SIZE (MAX_COMMAND_LENGTH + MAX_DATA_LENGTH + 2) + +// Set initial state to DEBUG to print start up info and commands. +// Set to WAITING_FOR_COMMAND to skip debug info. +static receive_state_t state = DEBUG; +static uint8_t rx_command[COMMAND_BUFFER_SIZE]; + +void clear_command_buffer() { + rx_command[0] = '\0'; +} + +void append_char_to_command_buffer(char c) { + size_t len = strlen(rx_command); + if((len+1) < sizeof(rx_command)) { + rx_command[len++] = c; + rx_command[len] = '\0'; + } +} + +void remote_init() { +} + +void remote_timer() { + //TODO: Setup pacing for large printf output to avoid Timer errors + static uint8_t tmp; + switch (state) { + case WAITING_FOR_COMMAND: + while(UART_BUFFER_COUNT(guart_buffer)) { + UART_BUFFER_RD(guart_buffer,tmp); + if (tmp == '\r' || tmp == '\n') { + if (sizeof(rx_command) == 0) { + //nothing interesting happened + continue; + } + state = PROCESSING_COMMAND; + break; + } else { + append_char_to_command_buffer(tmp); + } + } + break; + case DEBUG: + printf("\nSTARTING\n"); + printf("Ignoring input longer that %d characters\n", COMMAND_BUFFER_SIZE); + printf("Preparing %d available commands:\n", NUMBER_OF_COMMANDS); + for (size_t i = 0; i < NUMBER_OF_COMMANDS; i++) { + printf("\n----\n%s\n----\n%s\n", Commands[i].name, Commands[i].help); + } + state = WAITING_FOR_COMMAND; + systick_flag = 0; + break; + case PROCESSING_COMMAND: + //misc command here becasue compile breaks if variable creation is the first thing after a case statement + printf("\n%s\n", rx_command); + char * cmd = strtok(rx_command, " "); + char * data = strtok(NULL, "\0"); + if (strcmp(cmd, "HELP") == 0) { + clear_command_buffer(); + state = DEBUG; + break; + } + for (size_t c = 0; c <= NUMBER_OF_COMMANDS; c++) { + if (c == NUMBER_OF_COMMANDS) { + printf("ERROR Received invalid command \"%s\"\n", cmd); + printf("HINT Commands are case sensitive. Type HELP for list of commands.\n"); + } else if (strcmp(cmd,Commands[c].name) == 0) { + if(Commands[c].handler) { + Commands[c].handler(data, &Commands[c]); + } + //printf("%s\n", Commands[c].help); + break; + } + } + + clear_command_buffer(); + state = WAITING_FOR_COMMAND; + systick_flag = 0; + break; + } +} diff --git a/software/remote.h b/software/remote.h new file mode 100644 index 0000000..4b13483 --- /dev/null +++ b/software/remote.h @@ -0,0 +1,14 @@ +/* +Remote control over serial +AUTHOR: Tom Haynes (code@tomhaynes.net) +DATE: September 11th, 2020 +*/ +#ifndef _REMOTE_H_ +#define _REMOTE_H_ + +#include + +void remote_init(); +void remote_timer(); + +#endif diff --git a/software/uart.c b/software/uart.c index f5805e4..f9202dc 100644 --- a/software/uart.c +++ b/software/uart.c @@ -7,6 +7,8 @@ #include "load.h" #include "ui.h" +uart_buffer_t guart_buffer; + void uart_init() { uint16_t uart_div = (F_CPU + BAUDR/2) / BAUDR; @@ -22,166 +24,50 @@ int putchar(int c) return c; } -static uint8_t cnt = 0; void uart_timer() { - static uint16_t timer = 0; - timer++; - //Output one item each systick, but only start output with F_LOG - if (timer == F_SYSTICK/F_LOG) { - timer = 0; - cnt = 1; - } + static uint16_t timer = 0; + static uint8_t cnt = 0; + timer++; + //Output one item each systick, but only start output with F_LOG + if (cnt || (timer == F_SYSTICK/F_LOG)) { + timer = 0; + cnt++; + if (cnt == 1) { + char status = ' '; + if (load_active) { + status = load_regulated?'A':'U'; + } + printf("%c %d ", status, error); + } else if (cnt == 2) { + printf("T %3u ", temperature); + } else if (cnt == 3) { + printf("Vi %5u ", v_12V); + } else if (cnt == 4) { + printf("Vl %5u ", v_load); + } else if (cnt == 5) { + printf("Vs %5u ", v_sense); + } else if (cnt == 6) { + printf("I %5u ", actual_current_setpoint); + } else if (cnt == 7) { + printf("mWs %10lu ", mWatt_seconds); + } else if (cnt == 8) { + printf("mAs %10lu ", mAmpere_seconds); + } else { + printf("\r\n"); + cnt = 0; + } + } } -typedef enum { - STATE_IDLE, - STATE_COMMAND, - STATE_DIGITS, - STATE_WAITING_FOR_EXECUTION, - STATE_UNINITIALIZED, -} uart_state_t; - -static uart_state_t state = STATE_UNINITIALIZED; -static uint8_t cmd; -static uint16_t param; -static uint8_t error_code = 0; - -static inline void set_error(uint8_t code) +void uart_rx_irq() __interrupt(ITC_IRQ_UART2_RX) { - error_code = code; - error = ERROR_COMMAND; -} + //uint8_t tmp = UART2->DR; + while (UART2->SR & (uint8_t)UART2_FLAG_RXNE) { + UART_BUFFER_WR(guart_buffer, UART2->DR); + } -void uart_handler() -{ - if (cnt) { - if (cnt == 1) { - char status = 'D'; - if (load_active) { - status = load_regulated?'A':'U'; - } - printf("VAL:%c %d ", status, error); - } else if (cnt == 2) { - printf("T %3u ", temperature); - } else if (cnt == 3) { - printf("Vi %5u ", v_12V); - } else if (cnt == 4) { - printf("Vl %5u ", v_load); - } else if (cnt == 5) { - printf("Vs %5u ", v_sense); - } else if (cnt == 6) { - printf("I %5u ", actual_current_setpoint); - } else if (cnt == 7) { - printf("mWs %10lu ", mWatt_seconds); - } else if (cnt == 8) { - printf("mAs %10lu ", mAmpere_seconds); - } else { - printf("\r\n"); - cnt = 0; - } - if (cnt) cnt++; - } else if (error == ERROR_COMMAND) { - if (error_code) { - printf("ERR:%d %d %d\r\n", cmd, param, error_code); - error_code = 0; - } - } else if (state == STATE_WAITING_FOR_EXECUTION) { - printf("CMD:%c%d\r\n", cmd, param); + //TODO: Calibration mode - switch (cmd) { - case 'R': // Run - if (!load_active) { - ui_activate_load(); //This is handled in the UI code because we want to show the run mode on the display as well. - } - break; - case 'S': // Stop - if (load_active) { - ui_disable_load(); - } - break; - case 'M': // Mode - if (param < NUM_MODES) { - settings.mode = param; - } else { - set_error(ERR_MODE_INVALID); - } - break; - case 'c': // Setpoint CC - if (param >= CUR_MIN && param <= CUR_MAX) { - settings.setpoints[MODE_CC] = param; - } else { - set_error(ERR_OUT_OF_RANGE); - } - break; - case 'w': // Setpoint CW - if (param >= POW_MIN && param <= POW_MAX) { - settings.setpoints[MODE_CW] = param; - } else { - set_error(ERR_OUT_OF_RANGE); - } - break; - case 'r': // Setpoint CR - if (param >= R_MIN && param <= R_MAX) { - settings.setpoints[MODE_CR] = param; - } else { - set_error(ERR_OUT_OF_RANGE); - } - break; - case 'v': // Setpoint CV - if (param >= VOLT_MIN && param <= VOLT_MAX) { - settings.setpoints[MODE_CV] = param; - } else { - set_error(ERR_OUT_OF_RANGE); - } - break; - case 'E': // Store settings - settings_update(); - break; - case 'e': // Load settings - settings_init(); - break; - default: - set_error(ERR_INVALID_COMMAND); - } - cmd = 0; - param = 0; - state = STATE_IDLE; - } -} -void uart_rx_irq() __interrupt(ITC_IRQ_UART2_RX) -{ - char c = UART2->DR; - if (c == CMD_RESET) { - state = STATE_IDLE; - cmd = 0; - param = 0; - if (error == ERROR_COMMAND) { - error = 0; - error_code = ERR_NONE; - } - } else if (state == STATE_UNINITIALIZED) { - // Ignore everything till the interface is initialized - } else if (c == '\n' || c == '\r') { - if (state != STATE_IDLE) { - state = STATE_WAITING_FOR_EXECUTION; - } - } else if (state == STATE_IDLE) { - cmd = c; - param = 0; - state = STATE_DIGITS; - } else if (state == STATE_DIGITS) { - if (c >= '0' && c <= '9') { - param *= 10; - param += c - '0'; - } else { - set_error(ERR_NOT_A_DIGIT); - error = ERROR_COMMAND; - error_code = 1; - } - } else { - set_error(ERR_SHOULD_NOT_HAPPEN); - } - //TODO: Calibration mode } diff --git a/software/uart.h b/software/uart.h index 0dfd4dd..b9dc896 100644 --- a/software/uart.h +++ b/software/uart.h @@ -1,5 +1,6 @@ #ifndef _UART_H_ #define _UART_H_ +#include void uart_init(); void uart_timer(); @@ -16,4 +17,24 @@ typedef enum { ERR_INVALID_COMMAND, } error_codes_t; +#define UART_BUFFER_SIZE (128) //Must be a power of 2 +#define UART_BUFFER_MASK (UART_BUFFER_SIZE - 1ul) + +// Buffer read/write macros +#define UART_BUFFER_RESET(uart_buffer) {uart_buffer.rdIdx = uart_buffer.wrIdx = 0;} +#define UART_BUFFER_WR(uart_buffer, dataIn) {uart_buffer.data[UART_BUFFER_MASK & uart_buffer.wrIdx++] = (dataIn);} +#define UART_BUFFER_RD(uart_buffer, dataOut){uart_buffer.rdIdx++; dataOut = uart_buffer.data[UART_BUFFER_MASK & (uart_buffer.rdIdx-1)];} +#define UART_BUFFER_EMPTY(uart_buffer) (uart_buffer.rdIdx == uart_buffer.wrIdx) +#define UART_BUFFER_FULL(uart_buffer) ((UART_BUFFER_MASK & uart_buffer.rdIdx) == (UART_BUFFER_MASK & (uart_buffer.wrIdx+1))) +#define UART_BUFFER_COUNT(uart_buffer) (UART_BUFFER_MASK & (uart_buffer.wrIdx - uart_buffer.rdIdx)) + +/* buffer type */ +typedef struct { + uint32_t size; + uint32_t wrIdx; + uint32_t rdIdx; + uint8_t data[UART_BUFFER_SIZE]; +} uart_buffer_t; +extern uart_buffer_t guart_buffer; + #endif diff --git a/software/ui.c b/software/ui.c index 64244d5..9ef9def 100644 --- a/software/ui.c +++ b/software/ui.c @@ -26,19 +26,6 @@ typedef enum { DISP_MODE_BLINK_SLOW = 0b100, } display_mode_t; -typedef enum { - EVENT_BITMASK_MENU = 0b11, - EVENT_ENTER = 0b10, - EVENT_RETURN = 0b11, - EVENT_BITMASK_ENCODER = 0b11100, - EVENT_ENCODER_UP = 0b10000, - EVENT_ENCODER_DOWN = 0b10100, - EVENT_ENCODER_BUTTON = 0b11000, - EVENT_RUN_BUTTON = 0b11100, - EVENT_TIMER = 0b100000, - EVENT_PREVIEW = 0b1000000, -} ui_event_t; - static volatile int8_t encoder_val = 0; static volatile bool encoder_pressed = 0; static volatile bool run_pressed = 0; diff --git a/software/ui.h b/software/ui.h index 262a68d..88317e7 100644 --- a/software/ui.h +++ b/software/ui.h @@ -4,6 +4,19 @@ #include #include "menu_items.h" +typedef enum { + EVENT_BITMASK_MENU = 0b11, + EVENT_ENTER = 0b10, + EVENT_RETURN = 0b11, + EVENT_BITMASK_ENCODER = 0b11100, + EVENT_ENCODER_UP = 0b10000, + EVENT_ENCODER_DOWN = 0b10100, + EVENT_ENCODER_BUTTON = 0b11000, + EVENT_RUN_BUTTON = 0b11100, + EVENT_TIMER = 0b100000, + EVENT_PREVIEW = 0b1000000, +} ui_event_t; + void ui_init(); void ui_timer();