SPI device, 30 registers, is a shadow data pattern bad? - Page 1
Forum section: electronics / general

author: jnz
author_rank: Frequent Contributor
timestamp: September 26, 2020, 04:00:37 am
content: I have a device that doesn’t have a factory library. Eight bit command, eight bit data. 30 registers or so. Basic process is read, modify, write, read again to confirm. Single confined RTOS task, no thread safe issues.I have a lot of latitude and how I write the driver for this. I considered the following ideas:1.Shadow data. I have one two dimensional array. A list of registers going down and “write” / “read” columns as well. I read into it; copy to the write element, modify and that’s what gets written. I overlay a big structure with all the bitfields to manipulate. I am able to store the latest reads as well as the latest programmed. The write function would look like: devicewrite(register); and inside there I just look up the static array and find the write data for this register. I can’t really see what’s wrong with this. It’s a little less memory efficient but in this case I have plenty of ram to store multiple copies of this chip’s fields. It might be naughty that it’s static to the whole file?2.Similarly a big structure, no array and not statically used. So devicewrite(register, *writedata); this is less sharing memory, but I guess I don’t really see a safety difference. Depending how I do it, there might be some casting to the correct structure type like when I read back.3.Bit masks and fields. I need to write a whole byte at a time, but I don’t need to care about the whole byte. devicewrite(register, signal_mask, signal_pos, signal_value); is fine, but doesn’t use the bfm cpu instructions and is generally slower and more of a pain to read. I also need to list out all signals mask and lengths specifically as defines.4.You tell me.

author: ataradov
author_rank: Super Contributor
timestamp: September 26, 2020, 05:05:28 am
content: That depends entirely on the device and the register use.Most of the time you don't need to shadow anything, just write things as needed. In most cases you have a lot of configuration register, but just a few that are used all the time during the device operation.Just read and write registers as needed.

author: jnz
author_rank: Frequent Contributor
timestamp: September 27, 2020, 07:04:56 pm
content: I didn't really want to like this. I definitely see a debugging advantage of having a big structure with shadow data even if it MAY be out of date. BUT....Yea, in this case you're pretty much right. I don't really need to keep a big structure all the time. So instead, I laid out the deviceWrite( my_register_sturcture); where I take in an object of REG - READ - WRITE type and work on that specific register. IF later I did want whole shadow data array, I can just make it of register_structure_t and keep dealing with it using single register "sets" at a time. This lets me have the option of shadow storage, or just handle each register as I feel like it.The pattern is pretty much still READ, MODIFY, WRITE, READ, but if I am considering if a deviceWriteWithMask( reg_structure, mask_byte); is worth it. Saves a READ SPI command as I don't care what's in there now.I still think embedded programmers should do a better job of identifying and writing about design patterns like this.

author: ataradov
author_rank: Super Contributor
timestamp: September 27, 2020, 07:39:37 pm
content: I programmed a lot of SPI devices in my life and I never needed to have the entire register map. Even RMW operations are extremely rare. I think you are just overthinking it. When I need to configure the register I just write the whole register. Most of the time you can reassemble the value you wrote there previously from the application data.In some cases having the shadow copy is beneficial for one or two registers. This often comes up in interrupt handling stuff.But there is rarely a need to shadow the entire register map. It may be useful for debugging, but that's a different story.Hardware designers are also not stupid most of the time and make reasonable provisions for the controllers. So usually nothing particularly crazy is necessary.

author: Siwastaja
author_rank: Super Contributor
timestamp: September 28, 2020, 07:40:15 am
content: Write the simple, trivial implementation first. If you then see it's becoming messy, then think about abstracting it more. Premature over-thinking in good faith often leads to increased complexity and harder debugability.Typically, an SPI device with 30 registers described in datasheet, works out like this:1) You configure it once after powerup, with write-only operations to maybe 5 registers,2) You then use it by reading/writing 1-2 registers (typically no RMW),3) You may dynamically change some configuration setting by re-writing (still no RMW) one or two config registers,All of this works well with simple driver functions or ISR handlers that just access the registers as needed, with absolutely no extra abstraction inbetween.Having out-of-sync shadow data "for debugging" sounds like a huge trap for the future yourself. If you end up needing to debug the thing, do yourself a favour and read the value of interest at the time and place (in code) where you are interested about it.

author: Jeroen3
author_rank: Super Contributor
timestamp: September 28, 2020, 08:19:13 am
content: The only advantage I see to caching the entire config is when you'll have to withstand random power cycles.Otherwise it's just a potential trap that may violate the configuration order.

author: ajb
author_rank: Super Contributor
timestamp: September 28, 2020, 11:02:23 pm
content: Quote from: jnz on September 26, 2020, 04:00:37 amBe careful of padding/alignment, which are platform and compiler/setting dependent. The improvement in legibility may or may not be worth the potential lack of portability. As a general rule you need to be careful about using composite data structures across hardware boundaries, whether that boundary is switching compilers or going across an SPI link.QuoteThis, for preference. I actually like taking the time to transcribe a register map into defined offsets/masks. It forces me to read the entire map, which helps to understand the device and often prompts insights that otherwise would have taken longer to come across just by reading the datasheet text. Now if the device has HUNDREDS of registers, then yeah that's a chore, but a couple dozen isn't so bad and if the device is that complex then you'll need to spend a bit of time reading the datasheet anyway to understand its operation, and transcribing the register map isn't that big a deal. Throw the register map and any other device-specific code you write for it in a library for future use and you only have to do it once ever. If you have register operations that require a bunch of bit manipulation and you do them more than once, then you might want to write some macros inline functions to clean that up, but realistically it's not likely to be that big a deal, and to whatever extent it's harder to read the intent, it's very clear what is actually happening, which is what you really want in the low level driver code that will be manipulating the registers. Once you have that working, then you can build it into a nice library with common tasks wrapped up in functions if that makes sense for the application.

author: jnz
author_rank: Frequent Contributor
timestamp: September 29, 2020, 07:13:23 pm
content: Quote from: ajb on September 28, 2020, 11:02:23 pmGood points with one notable exception. For a test structure (tstr) and mask/position pairs, the structure is always much faster.QuoteBMF cpu instructions. Exist for a reason.So... This is what I ended on here. Yes, in this case a shadowdata set made no sense. Agreed with everyone above. However... There are variables I need to retain loop after loop that I can't just assume with one init. I can't just say "I set this field to X once and that's fine forever". I may need more RMW and RMWR than some devices can get away with.I agree with @ataradov that usually the chip designers do a good job of laying out the registers in a way that makes sense. But, there are definitely times when I want to modify a signal in a register vs write the whole register over.A lot of this comes down to what variables and settings are you storing loop over loop, how much can you recover from errors, what does your chip reset values look like, etc.So anyway... I landed on taking in a byte for address and a byte for data that is unioned over the structures, but I have a macro that masks that out when I want to only write to a specific signal. Likewise, I have a RMW, RMWR, W, etc. The one thing I didn't do specifically was to have defines for each mask and each position for the reason in the example above.

author: ajb
author_rank: Super Contributor
timestamp: September 29, 2020, 09:29:38 pm
content: ^^^ are those masks and positions defined constants or variables? If they're variables the results will depend on their scope and qualifications and on the optimization level. Likewise you will get different (and variable depending on scope, qualification, and optimization) results with functions in place of macros. Either way, it's usually not worth stressing about one or two instructions per access, especially at this stage. You'll never create a generic approach that is maximally efficient in all situations, so get it working first, make it clear, clean, and maintainable second, and then, *if* you have *actual* concerns about performance, worry about those extra instruction, once you have a complete picture of the way the software needs to work and can optimize effectively.Anyway the number of instructions per line of code is kind of meaningless without knowing which instructions and which platform, since not all instructions take the same number of cycles, and then it all goes out the window when you turn optimization on and the compiler is free to consolidate or reorder a lot of instructions.As far as storing values between loop iterations, that depends on what sort of values you have to deal with. Just like when dealing with internal peripherals, a given register may be volatile or not, and 'const' or not, and those require different handling. If you're changing bits here and there across a number of registers and doing that a LOT, then sure, maybe a shadow register set makes sense--I did that for an OLED display recently for exactly that reason--but that's of limited value if those registers are volatile because you'll have to check those every time (or so) anyway. If that's the situation, then I would hope that the device has bitwise set/clear registers, otherwise that will get painful to deal with.In any case, the SPI accesses will generally dominate the time it takes to deal with the peripheral, so your time is probably better spent making that as efficient as possible (DMA?) than worrying about whether it takes four instructions or five to access a particular shadow register.

author: jnz
author_rank: Frequent Contributor
timestamp: September 30, 2020, 06:13:39 pm
content: Quote from: ajb on September 29, 2020, 09:29:38 pmYea, it was just an example. ARM Cortex M4, -O1, defines, etc. Totally agree it's not worth worrying, but all the same, setting object.element = 4 is a whole lot easier to do without error than clearing bits by a mask and ORing in the pos shifted value. Not even getting into the BMF cpu instructions.QuoteAgreed. The time to just drop the CS pin low, start SPI with DMA, then bring CS back up again is longer than the time SCLK wiggles. The overhead is longer than the transfer. That's annoying when looking at effective SPI bitrate vs Mhz of SCLK.