How deep do you want to delve into this? You are a beginner programmer, and properly implementing this involves quite a few advanced data structures and methods.
I would use arrays of variable descriptor structures, one array per variable type that can be modified. These structures reside in ROM/Flash, and identify the location in RAM where the volatile variable is. I also like to store the hardware default value in this structure. Here is an example sketch you can compile and run on e.g. Arduino Leonardo (including Pro Micro clones, treating them as Arduino Leonardos in the Arduino environment):
// SPDX-License-Identifier: CC0-1.0
volatile int ivar;
static const char ivar_label[] PROGMEM = "ivar";
volatile int ifoo;
static const char ifoo_label[] PROGMEM = "ifoo";
volatile long lvar;
static const char lvar_label[] PROGMEM = "lvar";
volatile long lbar;
static const char lbar_label[] PROGMEM = "lbar";
volatile float fvar;
static const PROGMEM char fvar_label[] = "fvar";
// Helper function: Compares a string in RAM to a string in ROM/Flash, and returns true if they match.
static bool label_match(const char *ramlabel, const char *PROGMEM romlabel) {
while (1) {
const char ramchar = (unsigned char)(*(ramlabel++));
const char romchar = pgm_read_byte_near(romlabel++);
if (romchar == '\0' && ramchar == '\0') {
return true;
} else
if (romchar != ramchar) {
return false;
}
}
}
// Accessible volatile int variables with respective accessor functions
static const struct {
volatile int *const reference;
const char *const label PROGMEM;
int const initial;
} volatile_int[] PROGMEM = {
{ &ivar, ivar_label, 1 },
{ &ifoo, ifoo_label, 2 },
};
#define volatile_ints (sizeof volatile_int / sizeof volatile_int[0])
static inline volatile int *volatile_int_reference(const size_t i) { return (volatile int *)pgm_read_ptr_near(&(volatile_int[i].reference)); }
static inline const PROGMEM char *volatile_int_label(const size_t i) { return (const PROGMEM char *)pgm_read_ptr_near(&(volatile_int[i].label)); }
static inline int volatile_int_initial(const size_t i) { return pgm_read_word_near(&(volatile_int[i].initial)); }
static inline size_t volatile_int_find(const char *label) { for (size_t i = 0; i < volatile_ints; i++) if (label_match(label, volatile_int_label(i))) return i; return volatile_ints; }
// Accessible volatile long variables with respective accessor functions
const struct {
volatile long *const reference;
const char *const label PROGMEM;
long const initial;
} volatile_long[] PROGMEM = {
{ &lvar, lvar_label, 3 },
{ &lbar, lbar_label, 4 },
};
#define volatile_longs (sizeof volatile_long / sizeof volatile_long[0])
static inline volatile long *volatile_long_reference(const size_t i) { return (volatile long *)pgm_read_ptr_near(&(volatile_long[i].reference)); }
static inline const PROGMEM char *volatile_long_label(const size_t i) { return (const PROGMEM char *)pgm_read_ptr_near(&(volatile_long[i].label)); }
static inline long volatile_long_initial(const size_t i) { return pgm_read_dword_near(&(volatile_long[i].initial)); }
static inline size_t volatile_long_find(const char *label) { for (size_t i = 0; i < volatile_longs; i++) if (label_match(label, volatile_long_label(i))) return i; return volatile_longs; }
// Accessible volatile float variables with respective accessor functions
const struct {
volatile float *const reference;
const char *const label;
float const initial;
} PROGMEM volatile_float[] = {
{ &fvar, fvar_label, 5.0f },
};
#define volatile_floats (sizeof volatile_float / sizeof volatile_float[0])
static inline volatile float *volatile_float_reference(const size_t i) { return (volatile float *)pgm_read_ptr_near(&(volatile_float[i].reference)); }
static inline const PROGMEM char *volatile_float_label(const size_t i) { return (const PROGMEM char *)pgm_read_ptr_near(&(volatile_float[i].label)); }
static inline long volatile_float_initial(const size_t i) { return pgm_read_float_near(&(volatile_float[i].initial)); }
static inline size_t volatile_float_find(const char *label) { for (size_t i = 0; i < volatile_floats; i++) if (label_match(label, volatile_float_label(i))) return i; return volatile_floats; }
void setup() {
// Initialize accessible volatile int variables to their initial values
for (size_t i = 0; i < volatile_ints; i++) *volatile_int_reference(i) = volatile_int_initial(i);
// Initialize accessible volatile long variables to their initial values
for (size_t i = 0; i < volatile_longs; i++) *volatile_long_reference(i) = volatile_long_initial(i);
// Initialize accessible volatile float variables to their initial values
for (size_t i = 0; i < volatile_floats; i++) *volatile_float_reference(i) = volatile_float_initial(i);
Serial.begin(9600);
}
void loop() {
while (!Serial) /* Nothing */;
delay(10);
Serial.print(F("ivar = "));
Serial.print(ivar);
Serial.print(F(", ifoo = "));
Serial.print(ifoo);
Serial.print(F(", lvar = "));
Serial.print(lvar);
Serial.print(F(", lbar = "));
Serial.print(lbar);
Serial.print(F(", fvar = "));
Serial.println(fvar);
// increment ivar.
{
size_t i = volatile_int_find("ivar");
if (i < volatile_ints) {
(*volatile_int_reference(i)) += 1;
}
}
// negate fvar.
{
size_t i = volatile_float_find("fvar");
if (i < volatile_floats) {
volatile float *f = volatile_float_reference(i);
(*f) = -(*f);
}
}
// Delay for a second.
delay(1000);
}
The sketch uses minimal RAM. Compile and run it, and look at Serial Monitor:
ivar will increment, and
fvar be negated at every iteration.
The very first line with
SPDX-License-Identifier states that this file is
Licensed under CC0-1.0. It means you can do anything you wish with this file, but without any warranties or guarantees from the author or copyright holder (me).
The beginning of the sketch declares some volatile int, long, and float variables.
Each accessible volatile int, long, or float variable has an associated
_label stored in ROM/Flash.
Each accessible volatile type has an array in ROM/Flash (
volatile_int[],
volatile_long[], and
volatile_float[], above) of structures with three members: a
reference to the location of RAM for the volatile variable, a
label naming the variable – the label does not need to match the variable name! –, and its
initial value. You could omit the initial value stuff, but I included them since they are often very useful; and this way you can even "reset" a value to its initial value, simply by using the
volatile_type_initial(i) accessor function. They are used in
setup() to set the variables to their desired initial values.
(What are accessor functions? They are macro-like functions that access an array or data structure, hiding the complexities of such accesses. I could have implemented this as C++ classes, with proper accessor members, with a template to make it polymorphic (so it can be used with any scalar or structure data type you want).. but I didn't. One step at a time. As it is, the code above is compatible with C.)
The function
volatile_type_find(label) returns the index corresponding to the accessible volatile variable with the matching label, with
label in RAM. If there is no such variable, the function returns the size of the array (defined as a macro,
volatile_types).
If
i is a valid index, then
(*volatile_type_reference(i) refers to the variable value; it is equivalent to using the volatile variable itself (except tiny bit slower, if we are precise). To initialize that variable to its initial value, you can do
*volatile_type_reference(i) = volatile_type_initial(i);for example.
Because the descriptor structures are in ROM/Flash, adding new variables' information to the arrays (so they can be accessed) does not require any RAM. (Only the variables themselves are in RAM.)
The above example sketch does not include any serial input functionality. There are two basic approaches: One is to read serial input into a buffer, and when complete (for example, a newline is received), the buffer is parsed and acted upon. The other is a Finite State Machine that parses the input as it is received. The former is easier to implement, but typically uses more RAM than the latter; and the latter requires a well-formed input structure to be worth implementing as an FSM.
Essentially, when your serial input parser has a label for a variable, it must use
volatile_type_find(label) to find which, if any, of the arrays know a variable with that label. (Note that this also tells you the
type of the variable.) To set the value, or add, substract, multiply, or divide the value with some constant specified in the serial input, you then need to parse the serial input constant to the appropriate type, and finally update the value.
Thus, the above sketch really includes everything except the serial input parsing part. The
label_match() helper function can also easily be modified to end the comparison at whitespace or non-alphanumeric character, in which case returning 0 in case of mismatch, and positive length in case of full label match, makes the buffer-based parser much simpler. (You can very easily support not just assignment '=', but also addition '+=', subtraction '-=', and scaling '*=' and '/=', for numeric types, too, if you use serial input format with
label = value.)
Even at ten dozen lines of code, including comments and empty lines, there is LOTS of stuff to understand there.