DIY Modbus RTU fan controller, coils vs. registers - Page 1
Forum section: electronics / projects

author: Lomax
author_rank: Frequent Contributor
timestamp: July 14, 2023, 03:04:24 pm
content: Hi all,I'm building a Modbus RTU slave (my first ever) based on the ATMega328p, which is meant to control four fans. Each fan can run at one of three pre-set speeds, which are defined as 8-bit PWM duty cycles in three registers per fan. All registers are stored in EEPROM for persistence. The desired speed is selected with a 2-bit value, representing off, low, medium and high speed. These 2-bit values are also output on individual AVR pins, and used for driving LED indicators.In my initial version these values are also Modbus registers, and fan speed is selected by writing to those - but this approach has a drawback: since I write to the EEPROM every time a fan's speed is changed, this will eventually wear out. If each fan has its speed changed a handful of times per day the life-time of 100k writes suggested by the AVR datasheet would be exhausted in about a decade. This seems unnecessary, since my application has no need for the selected speeds to persist over power cycles - in factit may be preferable for all fans to be off at every power on, rather than having them return to the previous level.My first thought was to simply exclude those registers when writing to EEPROM, and to not trigger any writes when the speed is changed, but this seems a little messy and confusing. I generally tend to think of Modbus registers as contiguous and persistent, though I know there are no hard rules and operation varies wildly between RTUs. So I had a crazy thought: how about I control each fan's level through two "coils" instead? Activating an individual coil selects low or medium level, while activating both selects the high level. Seems like a great idea to me, but I have learned to be wary of great ideas, and thought I'd check what others think - maybe there's a better way? Perhaps I've missed something obvious? Questions & feedback appreciated!

author: RoGeorge
author_rank: Super Contributor
timestamp: July 14, 2023, 04:00:18 pm
content: I would only save to EEPROM the last values found in existence, right before the power down.To detect the shutdown moment, use the brown-out feature of the microcontroller. The brown-out is a hardware feature of the microcontroller, brown-out detector can generate an interrupt when the power supply falls under a certain voltage. The brown-out interrupt will write the last values into EEPROM. Make sure the onboard electrolytic capacitors can still supply energy for long enough (when the board is un-powered), such that the EEPROM save operation will have the time to write before running out of energy.

author: Lomax
author_rank: Frequent Contributor
timestamp: July 14, 2023, 04:02:47 pm
content: Quote from: RoGeorge on July 14, 2023, 04:00:18 pmThat's a neat idea, thank you!Edit:It won't prevent the selected levels from persisting though, so I'm still interested to hear what Modbus nerds think about using multiple coils to control a single device...

author: ajb
author_rank: Super Contributor
timestamp: July 14, 2023, 06:45:33 pm
content: If necessary, you can buy more time for a shutdown by providing a localized power domain for just the MCU, isolated from the rest of the logic supply via a diode (or transistor used for the same purpose), with its own bulk capacitance. That way, if you have a bunch of other logic or control circuitry, you don't need enough capacitance to hold ALL of it up for the shutdown time, just the MCU. The MCU can monitor the main logic supply voltage via an IO pin (or analog comparator if available) that triggers an interrupt to begin the shutdown procedure once the main voltage drops a little bit, rather than waiting for the MCU supply voltage to hit brownout levels. You'll need to take care to ensure that the other circuitry doesn't end up pulling power from the MCU supply during shutdown via a GPIO, and prematurely discharge the MCU reserve power.Is there generally a convention for modbus registers to be non-volatile? I would think that would be highly application- and device-dependent. I have very limited experience in this area, but I believe I've also seen devices where some registers are and some registers aren't non-volatile. As long as you document what the behavior is, I don't think either one would be an issue. I could also see an argument for a configuration option to control whether the speed setting is non-volatile, or perhaps two different registers where one is the non-volatile default speed at power on and the other is the volatile current speed setting. Lots of options to consider just depending on what behavior makes sense for the application.As far as using coils -- again, I have limited experience here -- it seems like that would be harder to deal with, since the coils aren't really independent from each other if they are combined to a single 2-bit value. If it were one coil for on/off and one coil for high/low speed, that would be different, but if you have off/low/med/high then you have to write both coils to set the speed instead of just writing to one register.

author: Siwastaja
author_rank: Super Contributor
timestamp: July 14, 2023, 06:55:33 pm
content: There are very few conventions in modbus. Persistence can exist or it does not, and may be implemented in various ways, it would be not weird to require the user to write to another register triggering a "save" operation!If you don't need persistence, just don't implement it; instruct the users to update the desired state regularly.Some simple wear-leveling wouldn't be too hard to implement either.

author: AndyC_772
author_rank: Super Contributor
timestamp: July 14, 2023, 07:10:07 pm
content: Decide what you'd like the end-user facing interface to look like, and implement that. If that means using a register for each fan, that's fine. If you'd rather use a couple of separate, individual bits ("coils"), then use those. Think of the user experience before anything else; it's why the original iPhone was so successful.Unless there's a really compelling reason, the interface you present shouldn't be tied to the underlying implementation - in this case, what, how and where you store data in EEPROM.If the registers need to be non-volatile, and you're concerned about the write endurance, then implement wear levelling. Divide your available EEPROM space into blocks, and in each one, store a unique sequence number along with a copy of the settings. Each time settings change, write them to a new EEPROM area along with the next sequence number. When your board powers up, search the EEPROM for the newest sequence number and use the settings associated with it.You will need to think about how to cope with wrapping around when the EEPROM fills up, as well as what happens if power fails during a write. I'll leave these as an interesting exercise for now - but just remember, you probably have a lot more EEPROM space than you need, so make use of it.

author: Lomax
author_rank: Frequent Contributor
timestamp: July 16, 2023, 10:42:13 am
content: Many thanks to everyone for their input! Since no one raised any compelling reason why using two "coils" to represent a 2-bit level might be bad I went ahead and re-wrote my code to do so. This ended up taking longer than I expected, but I now have something that's working. Please pardon my C - it is not my first language.Code: (C)#include <avr/io.h>#include <avr/interrupt.h>#include <avr/wdt.h>#include <avr/eeprom.h>#include "yaMBSiavr.h"// why is BAUD definition here ignored by #ifndef in yaMBSiavr.h?// #define BAUD 9600L#define channels 4#define regsize 16#define read_eeprom_array(address,value_p,length) eeprom_read_block ((void *)value_p, (const void *)address, length)#define write_eeprom_array(address,value_p,length) eeprom_write_block ((const void *)value_p, (void *)address, length)volatile uint8_t instate = 0;volatile uint8_t outstate = 0;volatile uint16_t EEMEM eeprom;volatile uint16_t registers;volatile uint8_t levels;static uint8_t const BTN0 = 0b00000100; //PORTCstatic uint8_t const BTN1 = 0b00100000; //PORTCstatic uint8_t const BTN2 = 0b00100000; //PORTBstatic uint8_t const BTN3 = 0b00000001; //PORTDvoid read_eeprom(void) {read_eeprom_array(eeprom, registers, regsize * 2);}void write_eeprom(void) {write_eeprom_array(eeprom, registers, regsize * 2);}void setup(void) {// output on B4,B5 (LEDs ch4) & B1,B2,B3 (PWM ch1-3)DDRB |= 0b00111110;// output on C0,C1 & C3,C4 (LEDs ch1 & 2)DDRC |= 0b00011011;// output on D6,D7 (LEDs ch3) & D3 (PWM ch4)DDRD |= 0b11001000;// pull-up on B0 (button input ch4)PORTB |= BTN3;// pull-up on C2,C5 (button inputs ch1 & 2)PORTC |= BTN0 | BTN1;// pull-up on D5 (button input ch3)PORTD |= BTN2;// UART clock on timer0TCCR0B |= (1<<CS01); //prescaler 8TIMSK0 |= (1<<TOIE0);// 8-bit Fast PWM on timer1 (on B1,B2)TCCR1A |= _BV(COM1A1) | _BV(COM1B1) | _BV(WGM10);TCCR1B |= _BV(CS12) | _BV(WGM12); // 30.52 Hz (prescaler 256)// 8-bit Fast PWM on timer2 (on B3,D3)TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);TCCR2B = _BV(CS21) | _BV(CS22); // 30.52 Hz (prescaler 256)// set all PWM to level 0OCR1A = registers;OCR1B = registers;OCR2A = registers;OCR2B = registers;}ISR(TIMER0_OVF_vect) {modbusTickTimer();}void setLevel(uint8_t channel, uint8_t level) {levels = level;switch(channel) {case 0: {OCR1A = registers[0 + level];PORTC = (levels | (levels<<3)) | BTN0 | BTN1;}break;case 1: {OCR1B = registers[4 + level];PORTC = (levels | (levels<<3)) | BTN0 | BTN1;}break;case 2: {OCR2A = registers[8 + level];PORTD = levels<<6 | BTN2;}break;case 3: {OCR2B = registers[12 + level];PORTB = levels<<4 | BTN3;}break;}}void applyOutstate(void) {for (int i = 0; i < channels; i++) {uint8_t state = (outstate >> (i*2)) & 0b00000011;setLevel(i, state);}}void incrChannel(uint8_t channel) {uint8_t new_level = (levels + 1) % 4;outstate &= ~(0b00000011<<(channel*2)); // Clear channeloutstate |= new_level<<(channel*2); // Set channelapplyOutstate();}void modbusGet(void) {if (modbusGetBusState() & (1<<ReceiveCompleted)) {switch(rxbuffer) {case fcReadCoilStatus: {modbusExchangeBits(&outstate,0,8);}break;case fcForceSingleCoil: {modbusExchangeBits(&outstate,0,8);applyOutstate();}break;case fcForceMultipleCoils: {modbusExchangeBits(&outstate,0,8);applyOutstate();}break;case fcReadHoldingRegisters: {modbusExchangeRegisters(registers,0,regsize);}break;case fcPresetSingleRegister: {modbusExchangeRegisters(registers,0,regsize);write_eeprom();applyOutstate();}break;case fcPresetMultipleRegisters: {modbusExchangeRegisters(registers,0,regsize);write_eeprom();applyOutstate();}break;default: {modbusSendException(ecIllegalFunction);}break;}}}int main(void) {read_eeprom();setup();sei();modbusSetAddress(0x01);modbusInit();wdt_enable(7);// TODO: enforce limits on register values// TODO: make PWM phase configurable// TODO: make PWM frequency configurable// TODO: make slave address configurable// TODO: make comm parameters configurable// TODO: move some stuff to a header file// TODO: button debouncingwhile(1) {wdt_reset();modbusGet();if((PINC & BTN0) == 0) {incrChannel(0);while((PINC & BTN0) == 0); // Wait until the button is released}if((PINC & BTN1) == 0) {incrChannel(1);while((PINC & BTN1) == 0);}if((PIND & BTN2) == 0) {incrChannel(2);while((PIND & BTN2) == 0);}if((PINB & BTN3) == 0) {incrChannel(03);while((PINB & BTN3) == 0);}}}I'm running this in Simulide, with a virtual serial port that I can talk to from a simple Python program:Code: (Python)#!/usr/bin/env python3import minimalmodbusinstrument = minimalmodbus.Instrument("/dev/pts/7", 1)instrument.serial.baudrate = 9600instrument.serial.timeout = 2while True:cmd = input("C/R:")if cmd == "r" or cmd == "R":register = input("Register:")if register:register = int(register)value = int(input("Value:"))instrument.write_register(register, value)val = instrument.read_registers(0, 16) # start, countprint(val)elif cmd == "c" or cmd == "C":coil =input("Coil:")if coil:coil = int(coil)on = int(input("1/0:"))instrument.write_bit(coil, on)val = instrument.read_bits(0, 8, 1) # start, count, fcprint(val)# set up two connected virtual serial ports with:# socat -d -d pty,link=/tmp/vtty1,raw,echo=0 pty,link=/tmp/vtty2,raw,echo=0I have lowered the AVR CPU frequency to 2 MHz and adjusted the clock prescalers accordingly - everything still works beautifully!

author: Lomax
author_rank: Frequent Contributor
timestamp: July 16, 2023, 07:27:06 pm
content: Quote from: RoGeorge on July 14, 2023, 04:00:18 pmI still think this is a very elegant solution, but will save it foranother project, which is much more complex (and has been on hold foryears).Quote from: ajb on July 14, 2023, 06:45:33 pmNice - and controlling this transistor from the MCU could have several advantages.Quote from: ajb on July 14, 2023, 06:45:33 pmI really don't know, but like you I also have quite limited experience. AFAICR the four other devices (from three manufacturers) on the bus that the fan controller will connnect to only have persistent r/w or persistent read only registers. I can't recall ever seeing anything else, but again I have limited experience. Or actually, one exception is a counter with a reset register which if you write to the counter will reset. Still, to my mind it seems natural that if I write a value to a configuration register then that value will automatically persist.Quote from: ajb on July 14, 2023, 06:45:33 pmIt's a little more complicated to deal with on the code side, but we're only talking about some bitwise-operation-magic - the hard part is designing those operations; bit of a mindbender for a C novice like me. The actual code is only a handful of extra lines, none of which will tax the CPU. Internally the coils are handled as bytes anyway, into which the eight I'm using fit very nicely (see code above if curious). I like the semantics of your suggestion to use one register for on/off and one for high/low, but I would have to forgoe the medium level which is not an option. And you would still have to "force multiple coils" to set the speed in those cases where the second bit was not already set to the desired speed.Quote from: AndyC_772 on July 14, 2023, 07:10:07 pmYeah, that's what I try to do, though the implementation is always guided by the language - I know it's good when the code doesn't feel forced, or arbitrarily contrived. I'm also very much a C novice, so have to keep it simple for that reason too.Quote from: AndyC_772 on July 14, 2023, 07:10:07 pmQuote from: Siwastaja on July 14, 2023, 06:55:33 pmAnother nice idea I hadn't thought of. Makes perfect sense, but seems rather overkill for this application. The registers are only really written to during commissioning, to configure the device and its channels. Since the active level of each channel is now set using coils the registers will only rarely need to be touched post deployment - certainly not with a frequency that could cause any significant wear.

author: ajb
author_rank: Super Contributor
timestamp: July 16, 2023, 09:10:10 pm
content: Nice!Quote from: Lomax on July 16, 2023, 07:27:06 pmI was actually thinking about the other side of the MODBUS interface, whatever is sending messages to control the fan controller. The register method allows a pretty direct translation from the intended operation to the required message: "set fan 1 to medium" simply becomes "fan1_register = value_that_represents_medium". On the other hand, with two coils representing three speeds and 'off', the desired state must be decomposed into two separate binary values. So "set fan 1 to medium" becomes "fan1_low_coil = off; fan1_high_coil = on" or whatever, where the boolean values have a less clear relationship to the intended state. Reading back that state, if you need to do that, is similarly more complicated, since you can't just look at one value to see if the fan is on. There are ways to deal with that, of course, but it seems like an unnecessary complication.

author: Lomax
author_rank: Frequent Contributor
timestamp: July 17, 2023, 12:08:38 am
content: Quote from: ajb on July 16, 2023, 09:10:10 pmUnderstood. I expect most "force multiple coils" implementations will take a start coil, a count and the bits for those, though accepted datatypes for the bits will vary. I use thenode-red-contrib-modbusmodbus-flex-write node and it wants an array of bits, which this JS one-liner will deliver:Code:value = 1;arr = new Array(2).fill().map((x, i) => { return (value >> i) & 1 });console.log(arr);[1,0]

author: ajb
author_rank: Super Contributor
timestamp: July 17, 2023, 02:59:59 am
content: It's not often we C programmers get to give the JS/Python crowd shit for being obtuse and not expressive

author: RoGeorge
author_rank: Super Contributor
timestamp: July 17, 2023, 06:54:18 am
content: First, congrats for making it work!About coils vs register, if you know your application can work OK without a register, then use coils. In theory, a register would be preferred.For the industrial process, a register would be safer than two coils (no matter it's written in Python or C), because writing a register is an atomic operation, all bits are changed at once.Why being atomic (which is usually a software consideration) would be important for a fan? Because, for example, you may want to increase the ventilation from fan=1 to fan=2 (in binary from 01 to 10). Notice you need to change both bits. If one of the two set-coil commands gives an error, the fan might end in 11, or in 00, instead of the desired 10. Trying to increase a fan, and ending with fan-off instead, could be very bad for the industrial process.If the fan values are meant to only increment/decrement by 1, then you can useGray Codeto encode the fan states. Grey code has the property that only one bit at a time changes (at an increment/decrement). In this case, would mean you will never need to send 2 set-coils commands. By choosing Grey code, the fan can be controlled with a single set-coil instead of two.If the fan states are not incremental, then the Gray code can not fix atomic problems, and to make sure both bits arrived well, you'll have to read back the state of the coils after each set-fan operation.

author: Siwastaja
author_rank: Super Contributor
timestamp: July 17, 2023, 08:19:53 am
content: Now that I think about it, having one fan per one modbus register would be easier to user, than mapping one fan across two registers. (Using register here as a generic term for something that has unique address.)Modbus devices are often controlled from all sorts of industrial programmable logic devices and while it is possible to make multi-register writes etc., it would be easier to explain and implement "write value 0-2 to register 20005" than to "write first bit to coil 10001 and second bit to coil 10002".In practice you can and will usewrite multiple coilsoperation for atomicity and communication efficiency, but how do you force users to always do that?

author: Lomax
author_rank: Frequent Contributor
timestamp: July 17, 2023, 10:54:40 am
content: Ok, you've convinced me with good arguments - I will revert back to using registersQuote from: ajb on July 17, 2023, 02:59:59 amNo I agree, C definitely seems a lot more bit friendly! That JS one-liner isawful, but it's the briefest possible way to perform this operation - with the possible exception ofCode:value = 1;arr = [0,0].map((x, i) => { return (value >> i) & 1 });console.log(arr);But then you lose the parameterisation of the bit-length. That said, I'm not sure there is a much prettier way to convert an integer to an array of bits in C?

author: Lomax
author_rank: Frequent Contributor
timestamp: July 17, 2023, 11:52:27 am
content: Ok, here's the updated code:Code: (C)#include <avr/io.h>#include <avr/interrupt.h>#include <avr/wdt.h>#include <avr/eeprom.h>#include "yaMBSiavr.h"// why is BAUD definition here ignored by #ifndef in yaMBSiavr.h?// #define BAUD 9600L#define channels 4#define regsize 20#define read_register_array(address,value_p,length) eeprom_read_block ((void *)value_p, (const void *)address, length)#define write_register_array(address,value_p,length) eeprom_write_block ((const void *)value_p, (void *)address, length)volatile uint8_t instate = 0;volatile uint8_t outstate = 0;volatile uint16_t EEMEM eeprom;volatile uint16_t registers;static uint8_t const BTN0 = 0b00000100; //PORTCstatic uint8_t const BTN1 = 0b00100000; //PORTCstatic uint8_t const BTN2 = 0b00100000; //PORTBstatic uint8_t const BTN3 = 0b00000001; //PORTDvoid loadRegisters(void) {read_register_array(eeprom, registers, regsize * 2);}void saveRegisters(void) {write_register_array(eeprom, registers, regsize * 2);}void setup(void) {// output on B4,B5 (LEDs ch4) & B1,B2,B3 (PWM ch1-3)DDRB |= 0b00111110;// output on C0,C1 & C3,C4 (LEDs ch1 & 2)DDRC |= 0b00011011;// output on D6,D7 (LEDs ch3) & D3 (PWM ch4)DDRD |= 0b11001000;// pull-up on B0 (button input ch4)PORTB |= BTN3;// pull-up on C2,C5 (button inputs ch1 & 2)PORTC |= BTN0 | BTN1;// pull-up on D5 (button input ch3)PORTD |= BTN2;// UART clock on timer0TCCR0B |= (1<<CS01); //prescaler 8TIMSK0 |= (1<<TOIE0);// 8-bit Fast PWM on timer1 (on B1,B2)TCCR1A |= _BV(COM1A1) | _BV(COM1B1) | _BV(WGM10);//TCCR1B |= _BV(CS20) | _BV(WGM12); // 31.25 KHz (prescaler 1)TCCR1B |= _BV(CS12) | _BV(WGM12); // 30.52 Hz (prescaler 256)// 8-bit Fast PWM on timer2 (on B3,D3)TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);//TCCR2B = _BV(CS20); // 31.25 KHz (prescaler 1)TCCR2B = _BV(CS21) | _BV(CS22); // 30.52 Hz (prescaler 256)}ISR(TIMER0_OVF_vect) {modbusTickTimer();}void setLevel(uint8_t channel, uint8_t level) {registers = level;switch(channel) {case 0: {OCR1A = registers[4 + level];PORTC = (registers | (registers<<3)) | BTN0 | BTN1;}break;case 1: {OCR1B = registers[8 + level];PORTC = (registers | (registers<<3)) | BTN0 | BTN1;}break;case 2: {OCR2A = registers[12 + level];PORTD = registers<<6 | BTN2;}break;case 3: {OCR2B = registers[16 + level];PORTB = registers<<4 | BTN3;}break;}}void updateLevels(void) {for(int i = 0; i < channels; i++) {setLevel(i, registers);}}void incrChannel(uint8_t channel) {uint8_t new_level = (registers + 1) % 4;setLevel(channel, new_level);}void modbusGet(void) {if (modbusGetBusState() & (1<<ReceiveCompleted)) {switch(rxbuffer) {case fcReadHoldingRegisters: {modbusExchangeRegisters(registers,0,regsize);}break;case fcPresetSingleRegister: {modbusExchangeRegisters(registers,0,regsize);updateLevels();saveRegisters();}break;case fcPresetMultipleRegisters: {modbusExchangeRegisters(registers,0,regsize);updateLevels();saveRegisters();}break;default: {modbusSendException(ecIllegalFunction);}break;}}}int main(void) {loadRegisters();setup();sei();modbusSetAddress(0x01);modbusInit();wdt_enable(7);updateLevels();// TODO: do not write EEPROM for registers 0-3// TODO: assemble LED pins into a byte that can be written to? virtual port?// TODO: simplify PORTC = (levels | (levels<<3)) | BTN0 | BTN1;// TODO: enforce limits on register values// TODO: make PWM phase configurable// TODO: make PWM frequency configurable// TODO: make slave address configurable// TODO: make comm parameters configurable// TODO: move some things to a header file// TODO: button debouncing (idelly non-blocking)// TODO: add reset button (factory defaults)while(1) {wdt_reset();modbusGet();if((PINC & BTN0) == 0) {incrChannel(0);while((PINC & BTN0) == 0); // Wait until the button is released}if((PINC & BTN1) == 0) {incrChannel(1);while((PINC & BTN1) == 0);}if((PIND & BTN2) == 0) {incrChannel(2);while((PIND & BTN2) == 0);}if((PINB & BTN3) == 0) {incrChannel(3);while((PINB & BTN3) == 0);}}}That's 23 lines less actual code, and it feels a lot more elegant - though I haven't figured out a nice way to "unpersist" registers 0-3. The level doesn't persist from button presses, but it does when modifying registers via Modbus. But then it might actually be desirable to be able to set the power-on level for each channel; perhaps the best route is to have a "save settings" register to write to which triggers the save (as suggested by@Siwastaja).I must have made a mistake somewhere though, because I now get blips in the level 0 PWM signal (255) that weren't there before:I've looked and looked but can't see where I'm doing some repeated call that might cause this interruption. The main loop does nothing unless there is an input (button press or Modbus command), so I don't understand

author: ledtester
author_rank: Super Contributor
timestamp: July 17, 2023, 12:41:49 pm
content: QuoteAnother idea... save the fan settings to the EEPROM but only when a certain MODBUS command is received (or register is written). That is, the user decides when the fan settings are saved.

author: ajb
author_rank: Super Contributor
timestamp: July 18, 2023, 02:18:25 am
content: Quote from: Lomax on July 17, 2023, 10:54:40 amWell, fundamentally any integer IS an array of bits. C provides minimal abstraction of this, so you just have to pick (or construct) the integer that gives you the right pattern of bits. You'd probably do something like:Code:#define FAN_MODE_OFF 0x0#define FAN_MODE_LOW 0x1#define FAN_MODE_MED 0x2#define FAN_MODE_HIGH 0x3or if you're feeling fancy:Code:typedef enum {FAN_MODE_OFF,FAN_MODE_LOW,FAN_MODE_MED,FAN_MODE_HIGH,} fan_mode_enum;It doesn't matter what numeric representation you use, but hex is conventional for values where the bit pattern is important. Enumerated values in C are guaranteed to start at zero and increment by one, and represent literal values just like#defines do, so the result is the same. The enum is just a convenient way to define the values. More complicated values might be constructed from bitwise operations, but those expressions are typically reduced to simple literal values during compilation.You'd expect to hand it to a function that looks something like:Code:void modbus_writeCoils(uint8_t stationAddress, uint16_t startCoil, uint16_t coilCount, uint32_t coilBits);// or if you want/have to write one at a time:modbus_writeCoil(addr, FAN_MODE_LOW_COIL, FAN_MODE_LOW);modbus_writeCoil(addr, FAN_MODE_HIGH_COIL, (FAN_MODE_LOW>>1) );Okay, I'll grant that none of these are really arrays in the sense that you mean, so if you REALLY want an array of bits, you could do:Code:bool fan_mode_low = {0,1};but each entry takes up at least one byte, so it's very storage inefficient--well, inefficient compared to bitfields. It's still way more efficient than an array of bits in python or JS

author: WattsThat
author_rank: Frequent Contributor
timestamp: July 18, 2023, 03:06:20 am
content: Quote from: ledtester on July 17, 2023, 12:41:49 pmThis +1. Make the write to eeprom dependent upon a Modbus register write with a specific value. Think of it as a “save config” command.

author: Lomax
author_rank: Frequent Contributor
timestamp: July 18, 2023, 06:39:55 am
content: Quote from: WattsThat on July 18, 2023, 03:06:20 amI guess this makes it +4?Quote from: Lomax on July 17, 2023, 11:52:27 am

author: Wiljan
author_rank: Regular Contributor
timestamp: July 18, 2023, 03:10:07 pm
content: I did make another arduino project where I needed to save some variable's when the power goes off, so they could be restored next time the power comes back.It's not an good idea to save often to the eeprom since it will fail then over timeI did use the comparator input and did make a resistor divider between the PSU +24v (before regulated to 5v) so the resistor divider then feed in a voltage on the comparator and if it goes below the threshold it generate an interrupt saving the variables to eeprom and stops.It's far faster than using the ADC to detect the power goes down, when thats said if you have many variables you want to store it takes time and you need the power to drop slowly.So in another project I did use FRAM with SPI (you can also have them with I2C) this is absolutly the best solution very fast and they remember without power so you can use them directly as variables... job donehttps://learn.adafruit.com/adafruit-i2c-fram-breakout/wiring-and-testhttps://learn.adafruit.com/adafruit-spi-fram-breakout

author: RoGeorge
author_rank: Super Contributor
timestamp: July 18, 2023, 03:59:11 pm
content: About FRAM, while not of a practical concern, it still wears out. Much slower than an EEPROM, but what's worst is that the FRAM wears out not only when writing, but it wears out when reading, too.In an endless loop of executing a JMP $ from FRAM, at 16MHz it will wear out in only 2 years.https://e2e.ti.com/support/microcontrollers/msp-low-power-microcontrollers-group/msp430/f/msp-low-power-microcontroller-forum/132022/fram-endurance

author: Wiljan
author_rank: Regular Contributor
timestamp: July 19, 2023, 07:55:02 am
content: Quote from: RoGeorge on July 18, 2023, 03:59:11 pmInteresting informationIf you only write to the FRAM when a variable actually does change in your code and only read it back when your system is powered on, then FRAM should be safe for more power-cycles than the power switch can handle