-
Notifications
You must be signed in to change notification settings - Fork 179
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
Controller Area Network (CAN) #215
Conversation
I see this as a feature, a la stm32-rs/stm32h7xx-hal#65, although I expect runtime remapping of CAN pins to be very unusual.
This seems fine and I agree with your justification, plus it's nice that correct usage is easily enforced by
I don't understand why this method is marked Otherwise this all looks good to me, I'm excited to start seeing more CAN development in embedded Rust! |
@timokroeger
For this reason I would propose to keep the possibility to choose between low level filter access and your high level one. Or creating a higher level interface would consume the lower level resources? What do you think? |
Thank you for the comments! @adamgreig Removed unsafe from Those are good points. I like your // can.rs
mod traits {
trait Filters {}
}
impl traits::Filters for Filters {}
impl traits::Filters for LowLevelFilters {}
fn take_rx(&mut self, _filters: impl Filters<Instance>) -> Option<Rx<Instance>> {
// ...
} Additionally, on stm32f105/107 the number of filters do not need to be split evenly between CAN1 and CAN2. This is also something we can make configurable with the lower-level filters. Ideally with const-generics as zero-cost abstractions (or with My plan is to finish this PR soon (without low-level filters) and to start working on embedded-hal traits for CAN. Shall include the filter traits as provision now or shall we skip it until the low-level filter code is ready? |
@timokroeger It is possible that even the low/high lever filters, RX, TX mailboxes may share behaviors (traits), only the configuration process is different... |
Is it possible to make a test suite for hardware, that can be run on 2 boards to check if everything works as expected? |
I have been playing with the CAN peripheral on an SMT32F413 and based on my experiments there I am not sure if #2 is actually correct. I was not able to get the loop-back mode to work without a a transceiver or at least a pull-up connected to the CAN TX pin. For some strange reason (and not matching the reference manual) it seems that the CAN peripheral will not exit config mode until it sees recessive states on the bus and being in loopback mode does not actually provide these recessive states for it to enter normal mode in my experiments. If you have a way to work around this I would be very interested though. That being said, I am hoping to have some time to test this work out on that f4. |
@arrowcircle @kevswims |
@timokroeger |
Most of the previously discussed "low-level" filter features can now be used with the @tib888
This is how I understood the purpose of this feature:
As reference the approach for the current interface:
Comparing those I do not see a substantial win in processing speed. Am I missing something here? |
I my understanding: to determine what to do with an incoming message, normally you should match the incoming id with many possible cases, which cost CPU time. Of course I do not have such time critical application, but in theory the processing can be speed up above described way. What do you think? |
Thanks! That way the filter index feature makes more sense. After looking around a bit I found the zephyr driver which uses filtering like in your description. Overall it looks quite a bit more complex (also in terms of memory usage). |
I can imagine the implementation by having a callback vector (which is not too much memory): And if one adds a filter he can also add a handler function, which should be stored on the appropriate index: And the receiver should call the handler like this:
Then inserting a filter handler would be: |
any updates on this? |
f6a4c2d
to
36924ca
Compare
Rebased onto master. This PR works well as is. Independent of this PR there is active discussion on the embedded-hal traits for CAN and I have a prototype for embedded-hal in a separate branch. Just as I’m typing this I see CI: Looks like I gotta fix something for the stm32f100 and stm32f101 targets still |
@timokroeger Do we have any examples to check on the hardware? With one board (loopback mode or something like this) and two boards (sender and receiver)? |
@arrowcircle |
@timokroeger I tested examples with bluepill board. I hade to make few changes because of only one can interface on f103 mcu. Here is working code for RTIC example: //! Interrupt driven CAN transmitter with rtic.
//!
//! CAN frames are allocated from a static memory pool and stored in a priority
//! queue (min heap) for transmisison. To start transmission the CAN TX
//! interrupt has to be triggered manually once. With each successful
//! transmission the interrupt is reentered and more data is fetched from the
//! queue.
//! Received frames are simply echoed back. In contrast to the naive `can-echo`
//! example all messages are also correctly prioritized by the transmit queue.
#![no_main]
#![no_std]
use heapless::{
binary_heap::{BinaryHeap, Min},
consts::*,
pool,
pool::{
singleton::{Box, Pool},
Init,
},
};
use panic_halt as _;
use rtic::app;
use stm32f1xx_hal::{
can::{Can, Filter, Frame, Id, Rx, Tx},
pac::{Interrupt, CAN1},
prelude::*,
};
pool!(
#[allow(non_upper_case_globals)]
CanFramePool: Frame
);
fn alloc_frame(id: Id, data: &[u8]) -> Box<CanFramePool, Init> {
let frame_box = CanFramePool::alloc().unwrap();
frame_box.init(Frame::new(id, data))
}
#[app(device = stm32f1xx_hal::pac, peripherals = true)]
const APP: () = {
struct Resources {
can_tx: Tx<CAN1>,
can_tx_queue: BinaryHeap<Box<CanFramePool>, U8, Min>,
tx_count: usize,
can_rx: Rx<CAN1>,
}
#[init]
fn init(cx: init::Context) -> init::LateResources {
static mut CAN_POOL_MEMORY: [u8; 256] = [0; 256];
unsafe { cx.core.SCB.vtor.write(0x0800_0000) };
let mut flash = cx.device.FLASH.constrain();
let mut rcc = cx.device.RCC.constrain();
let _clocks = rcc.cfgr
.use_hse(8.mhz())
.sysclk(64.mhz())
.hclk(64.mhz())
.pclk1(16.mhz())
.pclk2(64.mhz())
.freeze(&mut flash.acr);
#[cfg(not(feature = "connectivity"))]
let mut can1 = Can::new(cx.device.CAN1, &mut rcc.apb1, cx.device.USB);
#[cfg(feature = "connectivity")]
let mut can1 = Can::new(cx.device.CAN1, &mut rcc.apb1);
// Select pins for CAN1.
let mut gpioa = cx.device.GPIOA.split(&mut rcc.apb2);
let can_rx_pin = gpioa.pa11.into_floating_input(&mut gpioa.crh);
let can_tx_pin = gpioa.pa12.into_alternate_push_pull(&mut gpioa.crh);
let mut afio = cx.device.AFIO.constrain(&mut rcc.apb2);
can1.assign_pins((can_tx_pin, can_rx_pin), &mut afio.mapr);
can1.configure(|config| {
// APB1 (PCLK1): 16MHz, Bit rate: 1000kBit/s, Sample Point 87.5%
// Value was calculated with http://www.bittiming.can-wiki.info/
config.set_bit_timing(0x001c_0000);
});
// Filters are required to use the receiver part of CAN2.
// Because the filter banks are part of CAN1 we first need to enable CAN1
// and split the filters between the peripherals to use them for CAN2.
// let mut can1 = Can::new(cx.device.CAN1, &mut rcc.apb1);
let mut filters = can1.split_filters().unwrap();
// To share load between FIFOs use one filter for standard messages and another
// for extended messages. Accept all IDs by setting the mask to 0. Explicitly
// allow to receive remote frames.
filters
.add(&Filter::new_standard(0).with_mask(0).allow_remote())
.unwrap();
filters
.add(&Filter::new_extended(0).with_mask(0).allow_remote())
.unwrap();
let mut can_rx = can1.take_rx(filters).unwrap();
// let mut can_rx = can1.take_rx(filters).unwrap();
can_rx.enable_interrupts();
let mut can_tx = can1.take_tx().unwrap();
can_tx.enable_interrupt();
let can_tx_queue = BinaryHeap::new();
CanFramePool::grow(CAN_POOL_MEMORY);
// Sync to the bus and start normal operation.
can1.enable().ok();
init::LateResources {
can_tx,
can_tx_queue,
tx_count: 0,
can_rx,
}
}
#[idle(resources = [can_tx_queue, tx_count])]
fn idle(mut cx: idle::Context) -> ! {
let mut tx_queue = cx.resources.can_tx_queue;
// Enqueue some messages. Higher ID means lower priority.
tx_queue.lock(|tx_queue| {
tx_queue
.push(alloc_frame(Id::new_standard(9), &[0, 1, 2, 4]))
.unwrap();
tx_queue
.push(alloc_frame(Id::new_standard(9), &[0, 1, 2, 4]))
.unwrap();
tx_queue
.push(alloc_frame(Id::new_standard(8), &[0, 1, 2, 4]))
.unwrap();
// Extended frames have lower priority than standard frames.
tx_queue
.push(alloc_frame(Id::new_extended(8), &[0, 1, 2, 4]))
.unwrap();
tx_queue
.push(alloc_frame(Id::new_extended(7), &[0, 1, 2, 4]))
.unwrap();
tx_queue
.push(alloc_frame(Id::new_standard(7), &[0, 1, 2, 4]))
.unwrap();
});
// Manually trigger the tx interrupt to start the transmission.
rtic::pend(Interrupt::USB_HP_CAN_TX);
// Add some higher priority messages when 3 messages have been sent.
loop {
let tx_count = cx.resources.tx_count.lock(|tx_count| *tx_count);
if tx_count >= 3 {
tx_queue.lock(|tx_queue| {
tx_queue
.push(alloc_frame(Id::new_standard(3), &[0, 1, 2, 4]))
.unwrap();
tx_queue
.push(alloc_frame(Id::new_standard(2), &[0, 1, 2, 4]))
.unwrap();
tx_queue
.push(alloc_frame(Id::new_standard(1), &[0, 1, 2, 4]))
.unwrap();
});
break;
}
}
// Expected bus traffic:
//
// 1. ID: 007 DATA: 00 01 02 04 <- proper reordering happens
// 2. ID: 008 DATA: 00 01 02 04
// 3. ID: 009 DATA: 00 01 02 04
// 4. ID: 001 DATA: 00 01 02 04 <- higher priority messages incoming
// 5. ID: 002 DATA: 00 01 02 04
// 6. ID: 003 DATA: 00 01 02 04
// 7. ID: 009 DATA: 00 01 02 04
// 8. ID: 00000007 DATA: 00 01 02 04 <- extended frames have the lowest priority
// 9. ID: 00000008 DATA: 00 01 02 04 and reach the bus last
//
// The output can look different if there are other nodes on bus the sending messages.
loop {
cortex_m::asm::wfi();
}
}
// This ISR is triggered by each finished frame transmission.
#[task(binds = USB_HP_CAN_TX, resources = [can_tx, can_tx_queue, tx_count])]
fn can_tx(cx: can_tx::Context) {
let tx = cx.resources.can_tx;
let tx_queue = cx.resources.can_tx_queue;
tx.clear_interrupt_flags();
// There is now a free mailbox. Send the next frame if there is still someting
// in the queue.
while let Some(frame) = tx_queue.peek() {
match tx.transmit(&frame) {
Ok(None) => {
tx_queue.pop();
*cx.resources.tx_count += 1;
}
Ok(pending_frame) => {
// A lower priority frame was replaced with our high priority frame.
// Put the low priority frame back in the transmit queue.
tx_queue.pop();
if let Some(frame) = pending_frame {
tx_queue
.push(CanFramePool::alloc().unwrap().init(frame))
.unwrap();
}
}
Err(nb::Error::WouldBlock) => break,
_ => unreachable!(),
}
}
}
#[task(binds = CAN_RX1, resources = [can_rx, can_tx_queue])]
fn can_rx1(cx: can_rx1::Context) {
// Echo back received packages with correct priority ordering.
loop {
match cx.resources.can_rx.receive() {
Ok(frame) => {
cx.resources
.can_tx_queue
.push(CanFramePool::alloc().unwrap().init(frame))
.ok();
}
Err(nb::Error::WouldBlock) => break,
Err(nb::Error::Other(_)) => {} // Ignore overrun errors.
}
}
// Start transmission of the newly queue frames.
rtic::pend(Interrupt::USB_HP_CAN_TX);
}
}; |
Based on @arrowcircle example on stm32-rs#215
Thank you very much for testing @arrowcircle |
I have not been keeping track of the discussion in here, sorry about that. Is there anything left that you want to do before getting this merged |
All done, ready to merge! |
Awesome, yea it's indeed a big PR, thanks for putting in all the effort! I'll try to have a closer look at it before the weekend. In the meantime, it looks like there are a few new warnings found by the CI build |
4ad41ac
to
0f60df8
Compare
Based on @arrowcircle example on stm32-rs#215
Rebased to trigger CI |
Just found strange thing. After changing pins for CAN to PB8(RX) and PB9(TX) example code stopped working. These are only changes in the code: // Select pins for CAN1.
let mut gpiob = cx.device.GPIOB.split(&mut rcc.apb2);
let can_rx_pin = gpiob.pb8.into_floating_input(&mut gpiob.crh);
let can_tx_pin = gpiob.pb9.into_alternate_push_pull(&mut gpiob.crh); |
Great catch! What a big difference a single character can make inside a |
The example only works on devices with CAN peripheral
Previously the `can-loopback` example could not be built at all
Based on @arrowcircle example on stm32-rs#215
Big thanks to @TheZoq2
With this addition the frame constructors are now fallible and return `Error` when the identifier is out of range.
b53a11e
to
8d1732a
Compare
Added an |
* Fix typos and improve comments * Flatting return code checking * Use `nop` instead `wfi` so that debugging still works
Any updates on this? |
I completely forgot to have a look at this. Sorry for the delay, hopefully I'll get to it this week |
(false, true) => Ordering::Greater, | ||
// Ordering of the data/remote frames implicitly gives by the bit layout. | ||
_ => self.0.cmp(&other.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.
I don't think this computation is correct. Standard frames only have priority over extended frames when the most significant 11 bits of the IDs match.
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.
Thank you that is a great catch!
// The Equality traits compare the identifier and the data. | ||
impl PartialEq for Frame { | ||
fn eq(&self, other: &Self) -> bool { | ||
self.id() == other.id() && self.data[0..self.dlc] == other.data[0..other.dlc] |
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 PartialOrd
and PartialEq
implementations of a type must agree with each other. With this implementation that is not the case, since two data frames with the same ID but different data would compare equal via PartialOrd
, but not via PartialEq
.
Replaced by #293 |
This implementation is based on the experienced I collected when I picked up #101.
In comparison to the previous PR this is a more high-level approach.
My objective was to implement the minimal possible interfaces to support most use cases of the CAN peripheral.
Design Decisions
Separate
assign_pins()
A common pattern is to pass pins in the constructor of a peripheral API. This API takes a different two step approach where the constructor only enables the clock and takes ownership of the CAN pac peripheral.
A second call to
assign_pins()
is required to route the peripheral to the pins.It is needed for two special cases:
Possible edge case: The user properly configures the CAN pins without assigning them. The CAN peripheral works but does not own the pins.
Combined RX FIFOs
Both FIFOs are tightly coupled because they share acceptance filters: If a filter matches the incoming message it is put into just one of the FIFOs. In the common case where all messages shall be received FIFO1 remains completely unused because all messages are routed to FIFO0.
In my eyes splitting them introduces a leaky abstraction because they cannot really operate independently.
There is one interrupt for each FIFO. The downside of the combined approach is that both interrupts must be handled correctly as shown in the
can-rtfm
example.Combined Transmit Mailboxes
Required to prevent the inherent priority inversion problem. The problem is described well in the UAVCAN Specification v1.0-alpha Section 4.2.4.3.
This implementation is based on UAVCAN code. In addition lower priority frames can be swapped out to higher priority frames.
Unsafebit-timing configurationhttp://www.bittiming.can-wiki.info/ exists, works well and is convenient. It may be possible to do the same calculations in a const-fn. Not sure if it is worth the effort.
Testing
I tested the examples on a “18 in 1 Universal CAN Filter” Aliexpress board.
That’s the cheapest was to get a MCU with two CAN interfaces. For more details see this blog post.
TODO