-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
ports/esp32: CAN(TWAI) driver #9532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
a0a3587
to
daf1682
Compare
CAN(TWAI) driver must be in the machine module, same as UART, SPI, I2C and other communications. Please move CAN from the esp32 module to the machine module |
IhorNehrutsa Thank you for your response
The CAN itself is not platform specific, but current implementation is rather specific. That is rather questionable how to combine this API interfaces together if anybody in future would make |
ports/esp32/esp32_can.c
Outdated
|
||
if (msgs_to_rx == 1) { | ||
// first message in queue | ||
mp_sched_schedule(self->rxcallback, mp_obj_new_int(0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The identifier that is passed in the callback should be a constant that is exposed to the python side of things.
You are using mp_obj_new_int(0)
in this line and then in the lines below 1 and 2 are being done the same way. It is not going to be know what those values mean.
I think that it would be best to pass the actual message that is received and open up that receive buffer asap. You can collect the message information and pass it as a dictionary. This way you wouldn't have to bother with the 3 different integer values that are being passed to the callback. I would actually empty all of the receive buffers and pass an iterable of dictionaries. if a callback is received for TWAI_ALERT_RX_QUEUE_FULL then empty the buffer.
This would obviously only apply if the user registered a callback. The user should be able to single out what they want callbacks for. It look like having a callback is going to be a requirement in your code because there is no checking of rxcallback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The identifier that is passed in the callback should be a constant that is exposed to the python side of things.
Do you mean I should use MP_OBJ_NEW_SMALL_INT
instead if mp_obj_new_int
? That is my fault, I'll fix it. It could also be good idea to put values (0, 1, 2) to some constant or definitions
You can collect the message information and pass it as a dictionary.
I would like to make it in this way, but this API interface is made as close as possible to pyb.CAN
and according to pyb.CAN rxcallback documentation reason
argument should be one of 0
, 1
, or 2
and python-side code should look like
def cb0(bus, reason):
if reason == 0:
print('pending')
if reason == 1:
print('full')
if reason == 2:
print('overflow')
So there is no way (until massive refactoring and api-breaking changes) to make it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe the STM32 way of handling the callback is the best way to go about doing it. It is dependent on what appears to be a scheduler lock which I would think is going to unlock when it has time to. I would think that would be holding up the interrupt from being released. It also looks like the callback is made from inside the interrupt handler. until it makes the callback. It is also letting the buffer sit there and not get emptied. How large is the receive buffer is it large enough to wait to be emptied?.
If the interrupt pulled the message and passed to to the scheduler to make the call to the callback when here is time to tht would empty the buffer freeing up space and release the interrupt.
But at any rate..
This is the callback handler for the STM32
STATIC void can_rx_irq_handler(uint can_id, uint fifo_id) {
mp_obj_t callback;
pyb_can_obj_t *self;
mp_obj_t irq_reason = MP_OBJ_NEW_SMALL_INT(0);
byte *state;
self = MP_STATE_PORT(pyb_can_obj_all)[can_id - 1];
if (fifo_id == CAN_FIFO0) {
callback = self->rxcallback0;
state = &self->rx_state0;
} else {
callback = self->rxcallback1;
state = &self->rx_state1;
}
switch (*state) {
case RX_STATE_FIFO_EMPTY:
__HAL_CAN_DISABLE_IT(&self->can, (fifo_id == CAN_FIFO0) ? CAN_IT_FMP0 : CAN_IT_FMP1);
irq_reason = MP_OBJ_NEW_SMALL_INT(0);
*state = RX_STATE_MESSAGE_PENDING;
break;
case RX_STATE_MESSAGE_PENDING:
__HAL_CAN_DISABLE_IT(&self->can, (fifo_id == CAN_FIFO0) ? CAN_IT_FF0 : CAN_IT_FF1);
__HAL_CAN_CLEAR_FLAG(&self->can, (fifo_id == CAN_FIFO0) ? CAN_FLAG_FF0 : CAN_FLAG_FF1);
irq_reason = MP_OBJ_NEW_SMALL_INT(1);
*state = RX_STATE_FIFO_FULL;
break;
case RX_STATE_FIFO_FULL:
__HAL_CAN_DISABLE_IT(&self->can, (fifo_id == CAN_FIFO0) ? CAN_IT_FOV0 : CAN_IT_FOV1);
__HAL_CAN_CLEAR_FLAG(&self->can, (fifo_id == CAN_FIFO0) ? CAN_FLAG_FOV0 : CAN_FLAG_FOV1);
irq_reason = MP_OBJ_NEW_SMALL_INT(2);
*state = RX_STATE_FIFO_OVERFLOW;
break;
case RX_STATE_FIFO_OVERFLOW:
// This should never happen
break;
}
pyb_can_handle_callback(self, fifo_id, callback, irq_reason);
}
typedef enum _rx_state_t {
RX_STATE_FIFO_EMPTY = 0,
RX_STATE_MESSAGE_PENDING,
RX_STATE_FIFO_FULL,
RX_STATE_FIFO_OVERFLOW,
} rx_state_t;
This is how it should be written
STATIC void can_rx_irq_handler(uint can_id, uint fifo_id) {
mp_obj_t callback;
pyb_can_obj_t *self;
byte *state;
self = MP_STATE_PORT(pyb_can_obj_all)[can_id - 1];
if (fifo_id == CAN_FIFO0) {
callback = self->rxcallback0;
state = &self->rx_state0;
} else {
callback = self->rxcallback1;
state = &self->rx_state1;
}
mp_obj_t irq_reason = MP_OBJ_NEW_SMALL_INT(*state);
switch (*state) {
case RX_STATE_FIFO_EMPTY:
__HAL_CAN_DISABLE_IT(&self->can, (fifo_id == CAN_FIFO0) ? CAN_IT_FMP0 : CAN_IT_FMP1);
*state = RX_STATE_MESSAGE_PENDING;
break;
case RX_STATE_MESSAGE_PENDING:
__HAL_CAN_DISABLE_IT(&self->can, (fifo_id == CAN_FIFO0) ? CAN_IT_FF0 : CAN_IT_FF1);
__HAL_CAN_CLEAR_FLAG(&self->can, (fifo_id == CAN_FIFO0) ? CAN_FLAG_FF0 : CAN_FLAG_FF1);
*state = RX_STATE_FIFO_FULL;
break;
case RX_STATE_FIFO_FULL:
__HAL_CAN_DISABLE_IT(&self->can, (fifo_id == CAN_FIFO0) ? CAN_IT_FOV0 : CAN_IT_FOV1);
__HAL_CAN_CLEAR_FLAG(&self->can, (fifo_id == CAN_FIFO0) ? CAN_FLAG_FOV0 : CAN_FLAG_FOV1);
*state = RX_STATE_FIFO_OVERFLOW;
break;
case RX_STATE_FIFO_OVERFLOW:
// This should never happen
mp_obj_t irq_reason = MP_OBJ_NEW_SMALL_INT(RX_STATE_FIFO_EMPTY);
break;
}
I do not believe this line
mp_obj_t irq_reason = MP_OBJ_NEW_SMALL_INT(RX_STATE_FIFO_EMPTY);
should be there but to not break API that is how the function should be written. For readibility purposes this would be better
STATIC void can_rx_irq_handler(uint can_id, uint fifo_id) {
mp_obj_t callback;
pyb_can_obj_t *self;
mp_obj_t irq_reason = MP_OBJ_NEW_SMALL_INT(RX_STATE_FIFO_EMPTY);
byte *state;
self = MP_STATE_PORT(pyb_can_obj_all)[can_id - 1];
if (fifo_id == CAN_FIFO0) {
callback = self->rxcallback0;
state = &self->rx_state0;
} else {
callback = self->rxcallback1;
state = &self->rx_state1;
}
switch (*state) {
case RX_STATE_FIFO_EMPTY:
__HAL_CAN_DISABLE_IT(&self->can, (fifo_id == CAN_FIFO0) ? CAN_IT_FMP0 : CAN_IT_FMP1);
irq_reason = MP_OBJ_NEW_SMALL_INT(RX_STATE_FIFO_EMPTY);
*state = RX_STATE_MESSAGE_PENDING;
break;
case RX_STATE_MESSAGE_PENDING:
__HAL_CAN_DISABLE_IT(&self->can, (fifo_id == CAN_FIFO0) ? CAN_IT_FF0 : CAN_IT_FF1);
__HAL_CAN_CLEAR_FLAG(&self->can, (fifo_id == CAN_FIFO0) ? CAN_FLAG_FF0 : CAN_FLAG_FF1);
irq_reason = MP_OBJ_NEW_SMALL_INT(RX_STATE_MESSAGE_PENDING);
*state = RX_STATE_FIFO_FULL;
break;
case RX_STATE_FIFO_FULL:
__HAL_CAN_DISABLE_IT(&self->can, (fifo_id == CAN_FIFO0) ? CAN_IT_FOV0 : CAN_IT_FOV1);
__HAL_CAN_CLEAR_FLAG(&self->can, (fifo_id == CAN_FIFO0) ? CAN_FLAG_FOV0 : CAN_FLAG_FOV1);
irq_reason = MP_OBJ_NEW_SMALL_INT(RX_STATE_FIFO_FULL);
*state = RX_STATE_FIFO_OVERFLOW;
break;
case RX_STATE_FIFO_OVERFLOW:
// This should never happen
break;
}
pyb_can_handle_callback(self, fifo_id, callback, irq_reason);
}
I am not sure what this is
def cb0(bus, reason):
if reason == 0:
print('pending')
if reason == 1:
print('full')
if reason == 2:
print('overflow')
but if it has anything to do with the rx callback it is incorrectly written
If a buffer overrun did occur it would be incorrectly reported as the buffer being empty and the user would not be able to write code to properly address the problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are both on the same page as far as how it should work. the ESP32 while it is a peppy MCU a buffer overrun on the CAN interface can occur and it would be far better to collect the messages inside the interrupt handler if it can be done and pass that message to the scheduler to hand it off to the callback when it can.
That makes more sense to me to do it that way. The memory can be allocated for the number of messages it is able to do this with when the callback gets registered. an array of message structs can get made and one of the fields in the struct would be for marking the message as read so the interrupt handler would know if they could use that structure for an incoming message.This gives the user some ability to tweak it to suit their needs. I know you cannot allocate memory in an interrupt handler and I feel this would e an idea way to handle it. a static memory allocation for received messages and that allocation is able to be changed by unregistered the callback and registering it again with different number of messages to allocate.
Or at least give the user the option to do it that way or call the callback directly from the interrupt handler and not schedule the callback this way there is no delay and the user is able to act on it right away.
ports/esp32/esp32_can.c
Outdated
mp_raise_ValueError("CAN data field too long"); | ||
} | ||
tx_msg.data_length_code = length; | ||
tx_msg.identifier = args[ARG_id].u_int; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checking for proper frame ID. You are forcefully modifying the frame id that a user is passing instead of kicking back an error telling them that the frame id is out of bounds. you are also setting tx_msg.identifier
before a check is done as to the id type being used. Seems rather odd to do that twice.
One of the things that is done in DBC files is the setting of bit 30 in the frame id to identify the frame id bit depth. If it is a 1 it's extended and if it is a 0 then it is not. might want to consider using that. just an idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tx_msg.identifier = args[ARG_id].u_int;
Oh, that's literally left by chance, I'll fix it.
As for rising exception -- I agree with it, but stm32 implementation has literally the same code, that only converts identifiers to correct intervals (30 or 11 bits). It is even tested in the same way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just because of how it is done in the STM32 library does not make it right.
You do have it correct in setting the bit depth properly for both the 29 bit and the 11 bit so that is good. The reason why i suggest against just force setting it is if there is an error in someone's program and it is passing an incorrect identifier you could be stripping off part of the identifier that is valid and thus sending the data to the wrong node. If the node happens to exist on the network there could be behavior that is unwanted and without the error it is going to be difficult to track down the source of the problem.
this error
mp_raise_ValueError("CAN data field too long");
is unfounded. There is no maximum CAN data packet size for the ESP32. That is because of the TWAI_MSG_FLAG_DLC_NON_COMP macro. This allows more then 8 bytes to be sent in a can message. This should be added ad a parameter in the send function.
That is one example of why you cannot have a shared API between the CAN implementations. There is simply too many differences and if you omit features from one or the other there will be functionality that gets lost.
It's like the info function and returning a list of numbers. Well the ESP32 is going to provide a whole lot more information then what the STM32 does. Returning a dictionary or even a named tuple would be a better choice then just a list.
Not it happens to be by off chance that the STM32 and the ESP32 do share using the tseg1 and tseg2 to set the timing registers. Not al of them do. some use 2 different registers and some use 1. it all depends on the manufacturer and what is going to be used in the next up and coming MCU.
I don't personally like using the bs1 and bs2 keywords for the esp32 because this is not what it is called in the esp32 documentation it is called tseg1 and tseg2. and for other CAN interfaces it is called brt1 and btr2. And they are not necessarily calculated in the same way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am in agreement with you o a lot of this because tying to match the STM32 API is crazy because a lot is going to get lost.
For example if one was to keep the API the same then the loopback mode would need to be removed from the STM32 CAN. There is no loopback mode on the ESP32. nor is there a silent loopback mode.
TWAI_MODE_NORMAL
TWAI_MODE_NO_ACK
TWAI_MODE_LISTEN_ONLY
Those are the modes that are available for the ESP32
Alerts and states. ESP32 has these ones that are going to end up going unused.
CAN_STATE_RECOVERING
CAN_ALERT_BUS_OFF
CAN_ALERT_TX_FAILED
CAN_ALERT_BUS_ERROR
CAN_ALERT_ARB_LOST
CAN_ALERT_BUS_RECOVERED
CAN_ALERT_RECOVERY_IN_PROGRESS
CAN_ALERT_TX_SUCCESS
CAN_ALERT_TX_IDLE
TWAI_ALERT_PERIPH_RESET
I believe the ESP32 also has a pin that gets set to either a high or a low I cannot remember which and that pin can get connected to an input pin that can wake the ESP32. That pin can be read to know when the ESP32 should go to sleep if the bus is not active. IDK if that feature is available or not. That right there is a damned handy thing to have if say you are developing something for a car.
I don't see how they can be made to have a common API.
That's awesome that they finally added a callback for receiving. Happy that has been done. If the can module is to be moved to the machine module then the CAN implementation for the STM32 also has to be moved as well. It is currently located in it's own module called "CAN" The other issue with moving it to the machine module along with the STM32 CAN implementation is the gross differences in how the 2 operate. I am sifting through the ESP32 and the STM32 CAN code to see if a common API can be hammered out. Part of the issue with the STM32 CAN is the code was written so it is combining 2 different forms of CAN into a single entry point. It should not have been done this way as it really complicates the heck out of things. CAN FD and CAN are not the same thing and shouldn't have the same entry point. In order to correct things there is going to have to be API breaking changes made and there is not going to be a way around that. So this is the choice, change this PR so it makes a can module like is done for the STM32 or break the API for the STM32 and build a common entry point that will handle CAN and a separate entry point for CAN-FD and have them located in the machine module. |
@robert-hh |
Interested yes, but at the moment I cannot test it lacking suitable CAN hardware for connecting. |
I have 2 esp32's and also 2 can transceiver IC's. I can set up a breadboard and a VM with whatever OS you want and you can run all the tests you want. |
3e31c3b
to
6921294
Compare
I have updated the issues above + now it is building with idf4.4 and is disabled under idf4.0 because |
I know that the stm32 has the ability to accept only the bitrate in the constructor and it will calculate the timing registers needed to set up the interface. I have attached a file that does the same thing for the ESP32. This one is more in depth than the one that is available for the stm32 as it will calculate the SJW based on what the bus_length and transceiver_delay are. That provides an ideal balance of speed and robustness. it also uses a lot of calculation instead of all iteration to locate possible matches. A single bitrate can have multiple matches depending on what the sample point is. If a sample point below 50.0% is supplied then a CIA compliant sample point will be used. It handles the different BRP ranges for the different ESP32 versions. so no need to do anything special there I would also recommend not using the built in macros for getting the registers. They are not optimal ones to use and I believe that either one or more of them are actually incorrect. IDK if they have fixed this yet or not. I did open an issue with them about it. The other thing is depending on what the clock speed is you may not be able to dial exactly into the bitrate that is being requested. an example is 33,000bps/ This is not an achievable bitrate for the ESP32 but there can be a slight variation in the bitrate between nodes. That is what the SJW handles it's to keep things in sync. so there is an allowed deviation that can exist between nodes. The STM32 code does not take this into consideration so unless it finds an exact bitrate match you are outta luck. With this code it will return the closes possible match given the input values. It is attached. You will have to give it a test as I wrote it almost a year ago and there may still be a couple of glitches in it. |
For info: |
@FeroxTL Hi |
I can compile the firmware up for ya. You have to give me a day to do it tho. |
@kdschlosser That would be great!! You do not have to hurry, I'm actual on a business trip and won't be home before friday. The can tester which I'm currently try to get to work can be found on https://github.com/stko/canspy. It's in an early stage, I've just started... - I'm looking forward to your binary and many thanks in advance! |
Attaching a bit outdated version, but it 100% works. Could be flashed with command
|
Great! Sadly I'm on a business trip and don't have the device with me, but I'll try it out as as I'm back home on the weekend! Thanks a lot! |
|
I have just dowoloaded the latest nightly build firmware in terms of ESP32-S3-N8R8-OCT-SPRAM.It seems like CAN Module is not implemented yet.Not all the firmware support micropython CAN Module? |
Work perfectly. For esp32-c3+TJA1050 |
CAN bus for esp32 controller.
I just compiled this branch and it works well with my ESP32 (ESP32-D0WD-V3) - haven't got a transceiver at the moment but my logic analyser can decode the frames fine. |
I want to test it. Are there any docs? |
There is documentation for this module. Checkout to the brunch (esp32-can) and follow official instructions: https://docs.micropython.org/en/latest/develop/gettingstarted.html#building-the-documentation |
Hi, Device is not initialized
True
CAN(tx=19, rx=23, baudrate=500000kb, mode=LOOPBACK, loopback=1)
False
True
[0, 0, 0, 0, 0, 0, 0]
False [0, 0, 0, 0, 0, 2, 0]
Traceback (most recent call last):
File "<stdin>", line 47, in <module>
OSError: [Errno 116] ETIMEDOUT: ESP_ERR_TIMEOUT Im not shure if its the fault of the MCP2515 because its output pins are for SPI or if the setup I did was wrong. |
The MCP2515 is one driver using SPI. TWAI is the in-built driver. You should only add a transceiver, nothing else. |
Not shure if im getting it right. I have an ESP32 NodeMCU Development Board, and because the MCP2515 uses SPI its not viable in this situation. So do I just need to conmnect the CAN high an low to any gpio or do I need to connect a TJA1050 CAN Controller Interface? |
@FeroxTL and @thalesmaoa are correct. OK so the MCP2515 "module" is a can interface IC and also a transceiver packaged onto a board and that board has it's own oscillator on it and it will typically use SPI as an interface. The CAN that is built into MicroPython for the ESP32 is NOT for communicating with one of those kinds of interfaces. It is for using the built in interface the ESP32 has. The built in interface for the ESP32 does not have the transceiver so that would need to be added. It's a small inexpensive IC like what you see in the photo above. I am sure there is a CAN library available for your interface that runs on MicroPython. Do a search "MicroPython MCP2515" You will more than likely find several different implementations. The problem with the MCP2515 interfaces is that most of the manufacturers only provide an 8mHz oscillator. That means there are a lot of bitrates you will not be able to access. Where as the ESP32 uses an 80mHz clock and that opens up a lot more bitrates. |
The benefit of using the built in one is using 2 pins instead of 5. as well as the larger range of supported bitrates. |
Any chance we could get this merged into the main branch? Keen to use it in my projects! Just needs some conflicts resolving |
@FeroxTL Would you mind rebasing to fix the conflicts please? Really keen to get this into the main branch :-) |
Hi, I'm trying to use micropython with CAN to communicate between two custom ESP32 pcbs. With the upload provided by @stko (thank you, it helped a lot in getting started!), I got the communication up and running. I also tried to import this implementation into the latest masterbranch of micropython. To get it to compile, I had to remove the ESP_IDF MAJOR_VERSION ==4 requirements at two places, and I had to adjust a diferent CMake file (esp32_common.cmake). Running the tests, everything is working fine on the CAN_SILENT, CAN_LOOPBACK and CAN_SILENT_LOOPBACK modes. However, as soon as I try to run the CAN_NORMAL mode, I get an OSError: (-258, 'ESP_ERR_INVALID_ARG') error. From debugging, it appears that the brp argument is suddenly set to 0 inside the twai_driver_install() function in twai.c (found in the esp-idf package), whereas with the other modes it is properly set (val=16). The error happens as soon as I run: can = CAN(0, mode=CAN.NORMAL). I've tried to figure out what could be the cause of this, but I haven't found the reason yet. Does anybody have a clue what could be causing this problem? |
@FeroxTL Thanks for submitting this driver and keeping it up to date. We've decided on a plan for a cross-platform @FeroxTL @IhorNehrutsa I'd like to be sure we include everyone's considerations in the new API. To save me diff-ing many branches, can you please help me understand: Are there significant differences in approach between this PR and #12331 other than the choice of |
@projectgus I can confirm that it's just a copy-paste from an old PR. I just adapted it for modern micropython APIs and changed the location from Sorry, I do not have much time to support this PR. Tried to merge new changes with fast-forward, but it did not work. |
@FeroxTL No worries at all, I appreciate the confirmation. |
I'm going to close this out, then. We have #12331 open with similar changes against a newer master branch, and plans for the eventual CAN feature that will be merged. |
Add Seeed XIAO RP2350
Implemented CAN bus for esp32 controller.
This is based on #7381 and
ports/stm32/pyb_can.c
.All TODOs were implemented and tested using both real devices and programming tests (loopback mode).
APIs between this two implementations (esp32 CAN and stm32 CAN) are made as close as possible.
Some misunderstandings:
LOOPBACK_SILENT
mode). I didn't find any examples of similar behaviour in another testsNORMAL
mode. So if you have 100k CAN bus the baudrate should be 50000. I think it is a bug in esp-idf -- esp-idf's arduino port works in the same way (should also divide boudrate by two)