2023-08-09 08:03 PM - edited 2023-08-09 08:06 PM
I have an application that has to talk to 8 UART devices and a handful of other I2C and SPI devices. My current architecture is getting kind of complicated and I'd like to see how more experienced programmers handle a situation like this. I'll limit this to the UART code
I have a UART base class that provides basic UART functions like initialize, startDMAXfer, startITXfer and a few others. I have a separate file for each of the 8 UARTS that has a struct with the setup parameters specific to that UART like the DMA controller number, the RX and TX stream number, whether this specific port is DMA or Interrupt (IT) and so on. And each of these files also has the IRQ and DMA Handlers and callbacks for that specific port. The UART class also has an enumeration for every device connected to a UART, like a GPS receiver or a motor controller or an oceanographic instrument. Like this:
#pragma once
#include "USART1.hpp"
class UART
{
public:
UART()
{
}
enum devices
{
gps1 = 0,
ctd1,
pumpMotor,
msc1,
ocr1,
optode1,
flbb1,
lastDevice
};
void initUART(UART::devices name);
void startXfer(UART::devices deviceNum, uint8_t txBuff[], uint32_t txBuffSize, uint8_t rxBuff[], uint32_t rxBuffSize);
bool isMyDataReady(devices deviceNum);
private:
};
The code for any one device resides in its own file. Each device instantiates it's own version of the UART class and passes its deviceNum in the constructor like this.
UART ioPort;
ioPort.initUART(UART::gps1);
UART.initUART looks like this
void UART::initUART(UART::devices name)
{
switch(name)
{
case gps1:
usart1Init();
break;
}
}
and usart1Init() looks like this:
void usart1Init()
{
usart1::MX_USART1_UART_Init(); //This is straight from CubeMX inside STM32CubeIDE
//USART must be disabled to set character match character
LL_USART_Disable(USART1);
LL_USART_EnableIT_CM(USART1);
//Set character match character to LF (decimal 10);
USART1->CR2 |= (10 << 24);
LL_USART_Enable(USART1);
LL_USART_Disable(USART1);
LL_USART_DisableOverrunDetect(USART1);
LL_USART_ClearFlag_ORE(USART1);
//Stream 0 = RX
LL_DMA_EnableIT_TC(DMA1, LL_DMA_STREAM_0);
and so on
I was hoping this approach would 1) be reasonably readable and understandable by any developer with moderate C skills and a minimum understanding of classes. 2) Keep all the code specific to one UART in one file. 3) Provide functions common to all UARTs again with all the common code in one file and no cutting and pasting into other parts of the code. 4) Minimize unnecessary classes. For example, the UART specific files are namespaces, not classes, since the UART specific code is almost entirely a bunch of functions that don't interact or need to maintain any state variables. 5) Minimize coupling and maximize encapsulation. For example, only the UART class needs to #include the UART specific files.
The embedded background I have is with C# where everything basically has to be a class. However, my new design guideline is to only use a class where it makes the code more readable and/or reduces cutting and pasting. Regardless, I'm thinking that this problem has been solved by any number of more experienced programmers with and without classes and I'd appreciate comments and other alternatives.
Thanks
2023-08-09 08:49 PM
I found this post, which may be able to provide some hint:
https://controllerstech.com/managing-multiple-uarts-in-stm32/
I used these keywords:
2023-08-09 11:48 PM
@RhSilicon thanks for the quick response. I should have mentioned the main part of my app is a state machine that is doing a lot of things with the data from the devices. Most critical is a control loop that has to run at a pretty constant 100 times per second so I have a self-inflicted requirement to make sure I'm writing non-blocking code. I've got both DMA and Interrupt driven UART handlers working with the character match functionality built into the STM32H7 MCU so I don't have to do the kind of polling the ControllerTech article uses.
My questions are more at the architecture level. How do I break up the basic functionality into files: 1 file with everything in it or multiple files like I described above? Do I really need a UART class or should I just make that a set of functions in a namespace also? Instead of having one init function in the UART class with a swtich statement that calls the specified UARTs init code like I described above, is there a simpler, more readable way?
Thanks for help.
2023-08-10 05:51 AM
The programmer can do whatever is most advantageous for the program to work, including the use of reduced code (better known as bare-metal) is widely used, which can be obtained based on the HAL functions and using only the sequences of treatment of registers that the program uses, avoiding the many forks that the HAL code has.
About the structure, likewise, nothing prevents it from being done the way it was listed, the only thing that is interesting is having comments, and/or names of functions and variables that are easily identifiable, because after a while it can be difficult that the programmer himself who wrote the code can remember what he did.
There are many large open source projects on the Internet, it can be interesting to see how they were written and see what are the ways they were treated, usually the files are written in layers, as HAL itself is already done.
2023-08-10 02:46 PM
Sorry but this is all advice I can think of, this time...
2023-08-13 08:46 AM
@Pavel A. @RhSilicon Let me ask the question a different way. If you had an application that had to interface with 8 UARTs, how would you organize the various initialization, callbacks, IRQ handlers and other functions? There will be 8 IRQ handlers, 16 DMA stream IRQ handlers, 8 character match handlers, 8 UART config structs and so on. So lots of functions.
My preference is to use straight C but I have a basic understanding of C++ classes so if OOP helps make the code easier to understand by someone with a similar basic understanding of OOP, that is OK with me.
2023-08-13 09:12 AM
We are still a little far from that, but I believe that the days of programmers may be numbered, an artificial intelligence can write the code directly in the HEX file, or just program the MCU without any files, so there is not much that can be done about it, it is a matter of human limitation.
2023-08-16 04:11 AM - edited 2023-08-16 04:17 AM
Hi Gene,
Well this is a good question. When you try to be nice to others (or do you?) you indeed can take a route of "fancier design". From my limited experience, I prefer the "pragmatic" way: better testability, robustness and minimal dependencies. This usually pays off later.
In case of multiple internal UARTs: every UART and its DMA is individual, and can be tested as unit. Especially when the UARTs are in different modes: some may work with DMA, some with interrupts, some TX only, or half duplex, of flow control. I would implement each UART as a "software module" and test specifically to its role in the system and attached peripherals. For example, interrupt handler of some UART can do quick parsing or manipulate a timer. It is not obvious how much of common infra these UARTs should share.
Especially, I try to avoid function pointers in RAM, such as C++ virtual member function or objects in plain C, and various callbacks. These pointers can be easily corrupted. Whenever possible, make pointers read-only in ROM.
So it is not obvious whether a "high level abstraction" or "class" can be placed on top of all these UARTs. This can work with templates in modern C++, but IMHO amount of sophistication (and difficulty of debugging) is not worth it.
On the other hand, a multiport UART chip naturally can be presented as array of objects.
Bottom line... unless there is a specific requirement for something - keep it simple (this means prefer C, unless it is much easier/natural in C++).