ttlappalainen / NMEA2000

NMEA2000 library for Arduino
532 stars 220 forks source link

NMEA2000 with STM32 HAL #296

Open minoseigenheer opened 1 year ago

minoseigenheer commented 1 year ago

I started building a new STM32 NMEA2000 CAN library on top of the STM32 CAN HAL library. https://github.com/BitterAndReal/NMEA2000_STM32

I was not yet able to compile the NMEA2000 lib with STM32CubeIDE. Any help to get the NMEA2000 lib working without Arduino would be appreciated.

The buffer is copied from the NMEA2000_Teensyx lib by @ttlappalainen. Thanks for the great example!

I designed an STM32F105 based dual CAN bus controller with isolated NMEA2000 CAN. The design is available here. https://github.com/BitterAndReal/STM32_NMEA2000_CAN_gateway

ttlappalainen commented 1 year ago

Arduino IDE has Stream class and millis() function, which you do not probably have on STM32 IDE. There is file N2kStream.h, which defines tN2kStream class for Arduino. Take a look for https://github.com/ttlappalainen/NMEA2000_socketCAN/blob/master/NMEA2000_SocketCAN driver. That declares and defines tN2kStream class for linux. You need to do similar definition for STM32 driver.

For timers you may need to do same. On N2kTimer module there are definitions for tN2kScheduler and tN2kSyncScheduler classes and N2kMillis and N2kMillis64 functions. Those are now handled for Arduino, ESP and linux platforms and in generally for others by millis() funtion. E.g., ESP32 and linux supports 64 bit timers, which is a bit more efficient than using 32 bit timer. So if STM32 also has 64 bit timer, you should add timer definitions for STM32 special IDE. If it has only 32-bit timer, then just declare and define millis() function like in NMEA2000_socketCAN.

minoseigenheer commented 1 year ago

I defined the delay() and millis() function and figured out how to deal with the mix of C and C++. I finally managed to compile it with STM32CubeIDE but the ring buffer templates still give me hard faults. const CAN_message_t *msg = rxRing->getReadRef(); // ends up in the HardFault_Handler. CAN_message_t *msg = txRing->getAddRef(prio); // ends up in the HardFault_Handler too.

ttlappalainen commented 1 year ago

In constructo you should initialize ring buffers to 0 otherwise they may point to anywhere and on initialization you test that if ( rxRing == 0 ) .

Move from .h tNMEA2000_STM32 NMEA2000_STM32_instance; bool SetCANFilter( CAN_HandleTypeDef hcan, bool ExtendedIdentifier, uint32_t FilterNum, uint32_t Mask, uint32_t Filter ); to .c and make then static. They do not need to be visible.

Also on .cpp initialize static tNMEA2000_STM32 * NMEA2000_STM32_instance=0;

Then on constructor test is it 0. If it is not, for some reason someone is creating second instance of tNMEA2000_STM32, which you can then prevent doing anything.

On your main page you have long list of instructions to do to get it working. This is not idea of classes. When you do Open, just do all those settings. Constructor could be like tNMEA2000_STM32(io_pin _TxPin, io_pin _RxPin, tClockSet _ClockSet=72MHz); See example https://github.com/ttlappalainen/NMEA2000_esp32/blob/master/NMEA2000_esp32.h io_pin type here is pseudo type. It may be similar as for ESP32 gpio_num_t tClockSet is also STM32 dependent. If it already has definition for that type, use it. Other option is to define enum inside tNMEA2000_STM32. Set the most common clock as default value. Also pins can have default values.

minoseigenheer commented 1 year ago

Thank you for your help. I updated the STM32 CAN lib and it works for me so far. It is meant to be used with STM23CubeIDE and STM32 HAL. Different from Arduino you have a lot of freedom with STM32 MCU's but this brings some extra configuration steps. CubeIDE (Cube MX) has a nice visual interface for clock and peripheral configuration to make the setup beginner friendly. I will add a CubeIDE project example for the STM32 NMEA2000 CAN gateway... https://github.com/BitterAndReal/STM32_NMEA2000_CAN_gateway

minoseigenheer commented 1 year ago

How is sendFromTxRing(prio) called if CANSendFrame is not called? Should I call it from CAN TX Mailbox Empty callback? Can I use 0xFF as prio?

ttlappalainen commented 1 year ago

Check https://github.com/ttlappalainen/NMEA2000_Teensyx/blob/master/src/NMEA2000_Teensyx.cpp line 927

Flexcan has 8 mailboxes for sending so TxRing and mailbox index is same as message priority. If you have less mailboxes, you have to calculate related index for TxRing and mailbox. e.g., switch ( prio ) { 0..2: MbIndex=0; break; 3..4: MbIndex=1; break; 5..7: MbIndex=2; break; }

minoseigenheer commented 1 year ago

The new STM32 HAL CAN lib gives no control which TX mailbox is used. HAL_CAN_AddTxMessage() choses alway the fist empty mailbox. You can chose between lowest ID or FIFO mailbox priority. I use FIFO because lower ID priority falls back to lowest mailbox num if the ID is similar which could mess up the order of multiple frames with same id. Are you really worried about that there is no more ID priority once the frame is in the TX mailbox? And we have proper prioritising inside the ring buffer.

If I understand it I can then always call getReadRef( (uint8_t)0 ); with 0 = lowest prio

I added a STM23CubeIDE example project... https://github.com/BitterAndReal/STM32_NMEA2000_CAN_gateway/tree/main/STM32CubeIDE%20NMEA2000%20battery%20example

ttlappalainen commented 1 year ago

If you can not control used mailbox, it is useless and you can use that device only for receiving. Problem is that can controller mailboxes has different priorities. Lest expect you have 8 mailboxes 0-7 and 0 has highest priority. So if you now fill 0-3 mail boxes with single frame messages and send fast packet 8 frame message. First frames will be filled to mailboxes 4-7 and rest will wait on fifo buffer. Now mailbox 0 gets empty and controller starts to send frame from box 1. Interrupt system takes next frame from fifo and puts it to the mailbox 0. When frame from box has been sent, controllers starts again from highest mailbox 0 and interrupt takes frame from fifo and puts it to mailbox 1. Now you should see that fastpacket starting frame has not even been sent and order in over all for fastpacket frames is meshed up. This is exactly same problem as with default FlexCAN library used on Teensies or original mcp_can drivers. The only solution is that you pass totally HAL_CAN... and rewrite it on controller level.

It may look that it works and you get fast packet frames, but is not reliable and will loose fast packets time to time.

The reason why those CAN drivers works like that is that in CAN it does not matter - there is only single frame messages or TP messages, where frame sending is synchronized.

minoseigenheer commented 1 year ago

I don't understand. If I use FIFO inside mailboxes there should not be an ordering problem. That is what it is for. They get sent in the order they get put in the mailbox. The only issue is if I have the 3 mailboxes filled with lower prio messages and then want to send higher prio the lower prio messages still get sent first which delays the higher prio messages a tiny bit. Is that a problem? I could even use just one of the mailboxes and put it in the buffer if one of the mailboxes is pending, but then I loose the time it takes to fill the next message to the mailbox. But even then on chip mailbox filling is incredibly fast. In CANSendFrame I only put it directly to the mailbox if no message with higher prio is in the ring buffer. And in sendFromTxRing() I want to get the highest prio message from the buffer. How would I do that?

minoseigenheer commented 1 year ago

There is a very nice explanation here and how to work around. https://kentindell.github.io/2020/06/29/can-priority-inversion/

For CAN controllers with only three buffers then the software drivers must keep a bigger priority queue in memory and then shuffle the frames back and forth to keep the highest priority three frames in the hardware. All these controllers provide an an abort mechanism so that a low priority CAN frame in a hardware buffer can be thrown out and replaced by a new higher priority frame. The CAN controllers are generally designed to help with this: the bxCAN has a CODE field and the FlexCAN has a LPTM field to indicate which of the transmit buffers is the lowest-priority one.

ttlappalainen commented 1 year ago

No. This is different thing. You must take care that you never put fast packet frame to different mailbox as other same fast packet frames. If you do that, frame order may be mixed. And if you do not have control over mailboxes to be used, then you can not use it with NMEA2000. It may work, if driver has handling on deeper level that frame with specified priority will be always sent by specific mailbox.

minoseigenheer commented 1 year ago

I still don't understand your concern? The mailbox FIFO (first in first out) function will definitely not change the order of the messages. Think of it as it was one mailbox and the prioritising is fully handled by the ring buffer. Priority 0...7 go all in the same mailbox. How can the order be messed up if only one frame is in the mailbox at the time and we just put the next in if it is empty.

ttlappalainen commented 1 year ago

How many mailboxes that controller has? If there is only one mailbox, then it works, but is not very efficient. If there is more than one and you can not control which mailbox driver uses, then it does not work. I found text "The BxCAN includes 3 transmit mailboxes"

minoseigenheer commented 1 year ago

Yes it has 3 but you don't have to use all for sure. You can check the state of each individual mailbox and there is also an empty callback interrupt for each mailbox. But more importantly you can configure them in normal ID priority mode or FIFO mode.

In this mode the priority order is given by the transmit request order. This mode is very useful for segmented transmission like fast packet.

ttlappalainen commented 1 year ago

Sorry I did not read your original mail carefully enough and got that FIFO mode does makes all mailboxes equal. FIFO mode should work.

minoseigenheer commented 1 year ago

I did some tests and the system with the 3 fifo mailboxes seems to work. If you're using a single mailbox for each priority aren't you too slow to fill the next fast packet frame into the mailbox before a lower prio mailbox gets sent if there is one in a mailbox?

ttlappalainen commented 1 year ago

No. I do not know what do you mean with lower priority? If there is mailbox for each priority, then highest priority messages will be sent first as they should. By using single buffer and fifo mailboxes, higher priority frames will wait even lower priority frames so there is actually no priorities at all.

minoseigenheer commented 1 year ago

What I was trying to understand is, if you have lets say 10 fast packet frames and a few lower prio single frames in the TX buffer. From each priority one message is loaded into the corresponding mailbox. The highest priority message is sent and triggers the TX mailbox empty interrupt for this mailbox. The next message needs to be loaded from the buffer. But in the meantime the CAN controller already looks for the highest prio mailbox with a message waiting. Now there is probably no fast packet frame in the mailbox for a few microseconds and the lower prio frame which is waiting in the mailbox gets sent. It might be the case that you are quick enough and otherwise it's probably no problem at all but that is where the small hardware TX mailbox queue (fifo) is coming into play. The next message is always ready same priority as the previous one or not does not matter. The down side is we can have 3 lower prio frames in the queue and the higher prio has to wait. That is why they recommend to use the abort function to move lower prio messages back to the software buffer if a higher priority frame is in the buffer than the once's in the mailbox queue. I'm considering to add that to the STM32 CAN lib but it also ads complexity which is probably not necessary for most applications. Waiting for the 3 frames being sent can add a delay of a few milliseconds which is not perfect but also not horrible.

ttlappalainen commented 1 year ago

In Teensy case there is different mailboxes for different priority queues. So if one sends first 27 frame configuration information and then high priority position, it will be sent after first (or some) configuration information frame.

If you have one queue and fifo, then position will be sent after last configuration information frame so lot delayed.

If you have queues for all priorities and 3 frame fifo, then it should also be ok and not delayed more than 1-2 us.

minoseigenheer commented 1 year ago

I have an other question: When I want to use debugging in NMEA2000.cpp it uses DebugStream which is defined as Serial.print(...) but if we are not using Arduino this Class is not defined. I created a derived class N2kStream_STM32 and implemented the virtual write(), read() and peek() function to make sure it is not abstract. Then crated an instance called Serial in tNMEA2000_STM32. But I get an error: 'Serial' was not declared in this scope I'm probably doing something wrong.

ttlappalainen commented 1 year ago

Each debug messages are build with defines. So just add conditional define. E.g.,

#if defined(ARDUINO)
#define DebugStream Serial
#elif defined(STM32)
#endif

#if defined(NMEA2000_FRAME_ERROR_DEBUG)
  #if defined(ARDUINO)
    # define N2kFrameErrDbgStart(fmt, args...) DebugStream.print(N2kMillis()); DebugStream.print(": "); DebugStream.print (fmt , ## args)
    # define N2kFrameErrDbg(fmt, args...)     DebugStream.print (fmt , ## args)
    # define N2kFrameErrDbgln(fmt, args...)   DebugStream.println (fmt , ## args)
  #elif defined(STM32)
    # define N2kFrameErrDbgStart(fmt, args...) ...
  #endif
#else
# define N2kFrameErrDbgStart(fmt, args...)
# define N2kFrameErrDbg(fmt, args...)
# define N2kFrameErrDbgln(fmt, args...)
#endif
minoseigenheer commented 1 year ago

Ok sorry I expected it to work when declaring a Serial class, without a change to NMEA2000.cpp

minoseigenheer commented 1 year ago

I added this to NMEA2000.cpp

#if defined(ARDUINO)
#define DebugStream Serial
#elif defined(STM32)
#include "N2kStream_STM32.hpp"
N2kStream_STM32 DebugStream;
#endif