Just realized I probably didn’t actually answer your question.
In terms of how hard is it to get CAN working? At a basic level I’d say not ‘that’ much harder, but it’s much more open so it depends on what you’re trying to do with it.
I2C, chances are you’re hooking up signals from to the microcontroller to a device and using a library on the microcontroller that handles the hardware setup and control, which is normally just pin definitions and clock rate. From there, it’s looking at the documentation on the device, figuring out the address and then calling functions that will read/write the appropriate data to that address. Pretty easy, not a whole lot to go wrong.
Fundamentally, CAN won’t be that different. You’re hooking up signals from the microcontroller to a CAN transceiver and likely out onto a wire, then connecting that wire to other CAN devices. You’ll still use a library that sets up the CAN hardware for you. From there it gets a little trickier, in my experience. The hardware operates much more ‘in the background’, so instead of requesting data, you’re waiting for new messages to arrive or firing an interrupt when they do. There are also a lot more configuration options in terms of bit timing, potentially needing to define which address masks are being listened for, potentially setting up multiple different mailboxes to listen for specific messages. Some of this may be specific to the hardware I’ve used and may not be the ‘common’ generic implementation, I’m not sure. You also may need to define things like which messages will get acked etc.
If you’re using a pre-existing CAN device then it probably spits out frames at a specific rate and with pre-defined addresses. Reading data from those should be pretty straight forward. You’d define a CAN message that would be sent out (address, data loadout to match what was documented, send via a function) or define a message ID to listen for and then an interrupt handler (or wait loop) to act on that incoming frame, unpack it and do something with the received info.
If you’re using it in a situation where you’re talking between 2 devices, it’s much the same as above but you need to go through and define the message structure itself. At the simplest level this is easy, but it can get complex. Ideally you’d pay attention to which addresses you’re using, because lower numbered addresses have higher priority and will ‘win’ arbitration processes more often. You’d define frame repetition rates so that low update rate data might be 1 or 10Hz while fast update rate data might be up to 100Hz or beyond. You’d be checking that to make sure that you’re not overloading the bus and blocking the lower priority frames from transmitting, as well as having defined timeouts after which you flag an error due to a loss of communications to the other device. You’d need to make sure that both ends are using the same endianness and byte order, otherwise things can get real weird.
One nice thing about CAN is that due to it being a somewhat ‘higher level’ interface than I2C, there are a lot of options for mucking about with it. It’s relatively straightforward to use a PC with a USB-CAN interface to pretend to be a sensor or controller, for instance. We will often set up a project in PCAN Developer which pretends to be a device that we’re connecting to. That way instead of needing to test on an actual vehicle where we might not be able to make the battery management system do everything we want, instead we can end up with a slightly janky interface full of buttons and sliders that lets us set any values to anything we want. That way it’s a lot easier to test the firmware interfaces and that things like protection modes work, response times are good enough, error handling makes sense, etc.
So yeah, I’m still not sure I actually answered that question. I guess I’d say definitely more complex, but also some of that is just because you have a LOT more freedom with CAN, which brings with it its own complexity.