Enabling SJA1000 support on iMX6 for custom carrier board

We are exploring the possibility of migrating our applications, which currently run on top of WinCE7 on a T20 Colibri, to run on top of TorizonCore on a iMX6 Colibri.

We make custom carriers board which are mostly compatible with TorizonCore on a iMX6 Colibri, but our applications rely heavily on CAN communication and our custom board currently uses the SJA1000 CAN controller which we know is not supported out the box.

We know the iMX6 has an internal CAN controller which is supported on TorizonCore and it should be preferably used over an external CAN controller. We are working on a new custom board which will make use on the internal CAN controller, but in the meantime and also for backwards compatibility with our current board we wanted to know if it would be possible to use the SJA1000 CAN controller with the iMX6 Colibri and TorizonCore.

We expect that we’ll need to modify and build a custom device tree/overlay, but also on the CAN documentation we read that to enable SJA1000 support for the T20 Colibri the linux kernel needs to be recompiled with certain options and we were wondering if this was possible/necessary to enable it on the iMX6.

We found bindings and a driver in the linux kernel source. Could we instead use these to build and load the driver as external kernel module using TorizonCore Builder?

We read about the SocketCAN layer and it is our brief understanding that if we write software that uses this API (and maybe also the SAE J1939 protocol) and later down the line we change the CAN controller, the software will keep working independently of the hardware and the driver, is this correct?

Colibri iMX6DL 512MB V1.1A
Custom carrier board
TorizonCore Upstream 5.5.0+build.11 (dunfell)

Greetings @mmarcos.sensor,

Before I go in-depth into your question here I have a couple of question just to make sure I understand your situation.

So you’re looking to enable Linux kernel support for this “SJA1000” CAN controller on TorizonCore. However, you eventually plan to make use of the internal CAN controller on the i.MX6 going forward. But for compatibility reasons you’d like the option to have both of these if possible. Did I understand that all correct?

OS-wise do you plan to have 2 variations of your TorizonCore image? One that is configured for the SJA1000 and another that is configured for the internal CAN controller?

Or, are you going to have just one OS image that has both configured at the same time?

Best Regards,
Jeremias

@jeremias.tx thank you for replying so shortly.

So you’re looking to enable Linux kernel support for this “SJA1000” CAN controller on TorizonCore. However, you eventually plan to make use of the internal CAN controller on the i.MX6 going forward. But for compatibility reasons you’d like the option to have both of these if possible. Did I understand that all correct?

Your first assumption is correct, this was exactly our reasoning.

OS-wise do you plan to have 2 variations of your TorizonCore image? One that is configured for the SJA1000 and another that is configured for the internal CAN controller?

Or, are you going to have just one OS image that has both configured at the same time?

Regarding your questions: whether we plan to have 1 or 2 images, we are still contemplating the advantages or disadvantages of each case.

Our experience is that having 1 image is easier to maintain, but it adds an extra configuration step to production. Having 2 images is simpler for production but adds a maintenance burden. We could solve it with our CI server, having it automatically build both images from the same application source. There are lots of ways to go about it.

Why exactly do you ask? How does the answer vary in one case or the other?

Best regards,
Martin.

Hi @mmarcos.sensor,

Why exactly do you ask? How does the answer vary in one case or the other?

This doesn’t matter too much, it’s just good to know ahead of time in case the conversation goes deeper. Though let me begin answering your initial questions first of all.

we were wondering if this was possible/necessary to enable it on the iMX6.

Yes, it is possible to use SJA1000 CAN controller with the Colibri iMX6, but the kernel needs to be recompiled with additional configurations enabled. And as you said, the device tree has to be modified accordingly. Just to confirm do you know which exact kernel configs need to enabled for this controller? Looking at the driver source I can assume, but I just want to make sure we enable the correct things for you.

We know the iMX6 has an internal CAN controller which is supported on TorizonCore and it should be preferably used over an external CAN controller.

You also have to tinker with the device tree if you use the internal CAN controller, as its pins are not set by default in our Colibri modules. The CAN developer page you linked has additional information on how to do it.

Could we instead use these to build and load the driver as external kernel module using TorizonCore Builder?

The current TorizonCore upstream kernel already has the SJA1000 driver in it’s source (sja1000 « can « net « drivers - linux-toradex.git - Linux kernel for Apalis, Colibri and Verdin modules), so you don’t need to load it as a separate module. I can make the request to enable this as a kernel module by default once we confirm what configuration options you would need enabled.

later down the line we change the CAN controller, the software will keep working independently of the hardware and the driver, is this correct?

About the SocketCAN layer, you’re pretty much correct about it.

With all that said if you could confirm the exact kernel configurations you want enabled for the SJA1000, then I can proceed with the request to our team.

Best Regards,
Jeremias

@jeremias.tx

Actually we don’t know exactly what kernel options need to be enabled, we are pretty new to kernel configuration and compilation. How can we figure out what options are available and which ones need to be enabled for a desired functionality? Is there any documentation we can consult? Is there a command we can execute on the kernel source to get all available kernel options?

We only guessed that we would need to enable certain kernel options and recompile because we read it on the CAN documentation for the Colibri T20/T30. It says that kernel needs to be recompiled with the following options enabled:

CONFIG_CAN
CONFIG_CAN_RAW
CONFIG_CAN_BCM
CONFIG_CAN_DEV (depending on kernel version)

And, for de SJA1000 CAN controller which comes on the Evaluation Board revision 2.x, the following options as well:

CONFIG_CAN_SJA1000
CONFIG_CAN_SJA1000_PLATFORM

How can we be sure these are the same options that need to be enabled for the Colibri iMX6?

Yes, we saw it. We had some experience recently modifying the device tree and we think we can manage it when the time comes to switch to the iMX6’s internal CAN controller. At least this step doesn’t require reconfiguring and recompiling the kernel.

About the SocketCAN layer, you’re pretty much correct about it.

Great, that means we can start developing the application and it will still work when we switch the controller and driver.

Best regards,
Martin.

Actually we don’t know exactly what kernel options need to be enabled, we are pretty new to kernel configuration and compilation. How can we figure out what options are available and which ones need to be enabled for a desired functionality? Is there any documentation we can consult? Is there a command we can execute on the kernel source to get all available kernel options?

Looking at the list of options related to the SJA1000 here: Kconfig « sja1000 « can « net « drivers - linux-toradex.git - Linux kernel for Apalis, Colibri and Verdin modules

It looks like it only needs these options:

CONFIG_CAN_SJA1000
CONFIG_CAN_SJA1000_PLATFORM

I’ll see if this is something we can check before we add these to make sure.

With that said let me do this. I’ll make the request to our team to add the kernel config options for the above options related to the SJA1000. If there are no issues with adding these then the team should be able to add them relatively easy.

It will probably take the team roughly a week or so to add these. Once they’re added you should be able to then see these options enabled in a future nightly build of TorizonCore.

Does this sound all good to you? If yes, then I can go ahead with the request on my side.

Best Regards,
Jeremias

@jeremias.tx

It sound good to us.

Thank you for all your support.

Best regards,
Martin.

Alright then, the request for these configs has been made to the team. I’ll let you know when there is an update.

Best Regards,
Jeremias

Hi @mmarcos.sensor,

I was just informed that these configuration options should now be enabled by default in our kernel. This change should start showing up in our nightly builds in the next day or so.

Keep an eye out for this and let me know if you still don’t see this configuration enabled by default.

Best Regards,
Jeremias

@jeremias.tx,

I was just informed that these configuration options should now be enabled by default in our kernel. This change should start showing up in our nightly builds in the next day or so.

Can you confirm if the requested configuration options have been enabled in the nightly builds of TorizonCore?

Keep an eye out for this and let me know if you still don’t see this configuration enabled by default.

Where do we look for this? We have been browsing your git repositories but we couldn’t locate the corresponding configuration file. Could you link it?

In the TorizonCore Issue Tracker the issue still appears as open.

Best regards,
Martin.

As I said previously you can see for yourself that the config is enabled in the nightly as seen here:

zcat /proc/config.gz | grep SJA
CONFIG_CAN_SJA1000=m
# CONFIG_CAN_SJA1000_ISA is not set
CONFIG_CAN_SJA1000_PLATFORM=m

You can find this information by checking the kernel config on a running module.

The reason this issue is still open is because we haven’t had an official TorizonCore release yet with this change. Therefore it’s still considered “open” because it’s not released yet. But the change is still available in the nightly builds.

Best Regards,
Jeremias

Now that the nightly builds of TorizonCore have the driver enabled, we have been trying to draft a device tree configuration to use the SJA1000 CAN controller with the Colibri iMX6DL, but there is something very strange to us.

Our custom board was designed based on the Colibri Evaluation Board v2.1, which has an external SJA1000 CAN controller connected to the external memory bus of the Colibri SoMs. Most pins are routed as you would expect but pins ALE, WR# and CS# are connected to the output combination of a series of inverters and NAND gates of the following SoM signals: ADDRESS2 (SODIMM[115]), WR_N(SODIMM[99]) and CAN_CS#(SODIMM[105]).

Here is a screenshot from the Colibri Evaluation Board v2.1 schematics.

This layout works correctly with drivers for WinCE7 running on the Colibri T20.

On the screenshot above there is note under the SJA1000 symbol indicating that ADDRESS[2] is used to signal whether address or data is present on the bus, it is our understanding that this is precisely the purpose of the ALE signal. We would have thought that the ALE pin would somehow be connected to pin SODIMM[150] since both the Colibri T20 and iMX6 have a Valid Address signal routed to that pin(signals GMI_ADV_N on the T20 and EIM_LBA on the iMX6). We are oblivious to why ADDRESS[2] is used instead. Can you provide some insight?

Will the linux driver work with this layout or will it only work with drivers for WinCE7?

Our current draft is as follows:

&weim {
	status = "okay";

	/* weim memory map */
	ranges = < 0 0 0xc0000000 0x02000000 >;

	/* SJA1000 CAN Controller on Colibri EIM_CS0 */
	can3@0,0 {
		compatible = "nxp,sja1000";
		reg = <0 0x00000000  0x00000100>;
		interrupt-parent = <&gpio3>;
		interrupts = <27 0x2>;
		nxp,external-clock-frequency = <16000000>;
		fsl,weim-cs-timing = <0x07F13039 0x00001002 0x18683372
							  0x00000068 0xd863FFE6 0x00000000>;
	};
};

The fsl,weim-cs-timing property sets 6 uint32 cells which correspond to the 6 configuration registers for the n-th chip select:

  1. EIM_CSnGCR1: Chip Select n General Configuration Register 1
  2. EIM_CSnGCR2: Chip Select n General Configuration Register 2
  3. EIM_CSnRCR1: Chip Select n Read Configuration Register 1
  4. EIM_CSnRCR2: Chip Select n Read Configuration Register 2
  5. EIM_CSnWCR1: Chip Select n Write Configuration Register 1
  6. EIM_CSnWCR2: Chip Select n Write Configuration Register 2

We are still analyzing each of the configuration options and deciding which is correct to use with the SJA1000 and the driver.

Any suggestions will be greatly appreciated.

Best regards,
Martin.

So just to clarify on your carrier board are the signals hooked up to the SJA1000 identically to the Colibri Evaluation Board v2.1?

We would have thought that the ALE pin would somehow be connected to pin SODIMM[150] since both the Colibri T20 and iMX6 have a Valid Address signal routed to that pin(signals GMI_ADV_N on the T20 and EIM_LBA on the iMX6). We are oblivious to why ADDRESS[2] is used instead. Can you provide some insight?

As for this unfortunately I can’t comment here. V2.1 of the Colibri Evaluation Board is quite old and predates my work here at Toradex. I’ll see if I can get anyone on the hardware team to comment in on this.

Will the linux driver work with this layout or will it only work with drivers for WinCE7?

I don’t think there’s anything here that would explicitly prevent it from working. The Linux driver shouldn’t rely on specific pins. Furthermore in the device tree one can change the pinmuxing for interfaces for additional flexibility. Though of course this depends on how you physically have the hardware connected.

Best Regards,
Jeremias

So just to clarify on your carrier board are the signals hooked up to the SJA1000 identically to the Colibri Evaluation Board v2.1?

Exactly.

As for this unfortunately I can’t comment here. V2.1 of the Colibri Evaluation Board is quite old and predates my work here at Toradex. I’ll see if I can get anyone on the hardware team to comment in on this.

We have been investigating and it is possible that the way the Colibri Evaluation Board v2.1 was routed originated with the Colibri PXA320 or PXA270. Especially the PXA320, which has some peculiar address latching options as explained in section 2.6.7.1 of the Marvell® PXA3xx (88AP3xx) Processor Family Vol. II: Memory Controller Configuration Developers Manual.

I don’t think there’s anything here that would explicitly prevent it from working. The Linux driver shouldn’t rely on specific pins. Furthermore in the device tree one can change the pinmuxing for interfaces for additional flexibility. Though of course this depends on how you physically have the hardware connected.

The way the SJA1000 is routed on our carrier board (identical to the Colibri Evaluation Board v2.1) works perfectly with the SJA1000 drivers for WinCE7 running on a Colibri T20 which is strange because it is connected to the GMI memory controller and we couldn’t find anything in the T20 documentation about using an address line as a Valid Address signal, instead of the dedicated GMI_ADV_N line. So one hypothesis is that this complexity is managed by the windows driver which we don’t have the source code for so we can’t check. The other hypothesis is that using an address line and combining it with the WR# and CAN_CS# signals through logic gates as it has been done just works. If that’s the case we haven’t been able to reason how it works.

Our doubts whether the Linux driver would work or not comes from the fact we haven’t found any working examples or documentation of an iMX6 connected to an SJA1000, the few we did find were connecting the ALE pin of SJA1000 to the EIM_LBA line. We browsed the driver source code and it seems like it just expects the SJA1000 registers be mapped contiguous on the platform bus, and in that case whether or not it works depends on how the memory controller(the iMX6’s EIM) maps them into memory.

We are going to test different EIM configurations and probe the lines to see what we see and hopefully we can get to work.

Best regards,
Martin.

So far it doesn’t seem like anyone recalls or knows why exactly V2.1 of the Colibri Evaluation Board was designed that way specifically.

We have been investigating and it is possible that the way the Colibri Evaluation Board v2.1 was routed originated with the Colibri PXA320 or PXA270. Especially the PXA320, which has some peculiar address latching options as explained in section 2.6.7.1 of the Marvell® PXA3xx (88AP3xx) Processor Family Vol. II: Memory Controller Configuration Developers Manual.

This is definitely possible. Keep in mind the Colibri i.MX6 was released after we released V3.1 of the evaluation board. Therefore The Colibri i.MX6 was most likely not considered in the design of V2.1.

We are going to test different EIM configurations and probe the lines to see what we see and hopefully we can get to work.

Let me know how your tests go. As I said in theory the software should be flexible, but this depends on whether your hardware configuration can accommodate this. Which is to be seen.

Best Regards,
Jeremias

We figured how it works and despite of not having the source code we are fairly certain that it is the windows driver that is managing the added complexity to interface the SJA1000 with the aforementioned hardware layout.

In our archives we found a simple WinCE6 native C++ CAN demo meant to be used with a Colibri T20/PXA on top of the Colibri Evaluation Board v2.1. The demo doesn’t use the SJA1000 driver/library, instead configuration and interface routines are all done by the application software. Configuration for the GMI peripheral is what you would expect: all default values except for the SNOR_SEL field of the SNOR_CONFIG_0 register which is modified to select CS4. In this configuration the GMI peripheral interfaces the SJA1000 using an asynchronous 16bit muxed communication. In the code there are also two functions: GetReg to read data from a SJA1000 register and SetReg to write data to a SJA1000 register. All other SJA1000 related functions depend on these two functions.

The GetReg and SetReg functions are simple. When reading a register, it first writes the register address to bus address 0x0000 and then reads and returns register value from bus address 0x0004. Writing to a register is very similar, it first writes the register address to bus address 0x0000 and then writes register value to bus address 0x0004. These operations paired with the hardware layout of NAND gates and inverters makes it so that TWO separate GMI bus operations (1. writing register address to bus address 0x0000 and 2. reading or writing value from or to bus address 0x0004) are converted to only ONE valid memory access operation for the SJA1000. Since it uses address line 2 (A2), when writing to address 0x0000, all address lines including A2 are low, and when writing or reading from address 0x0004, all address lines are low except A2 which is high. We’ve attached some timing diagrams to help illustrate this behavior. We have confirmed this with logic analyzer and oscilloscope captures. Maybe we are describing a well known way to access memory but we hadn’t seen it before and we don’t know if there is a specific name for it.

Writing Operation:

Reading Operation:

Inspecting the linux driver source code we found there is an optional operation mode which can be set by assigning the compatible property of the SJA1000 device tree node to “technologic,sja1000”. This mode of operation was added to support a SJA1000 compatible IP from Technologic Systems that is instantiated in an FPGA but due to some bus widths issues, access to registers is made through what the committer calls a “window” mechanism (view commit). This mode of operation is essentially the same as we described in the previous paragraph with the exception that it seems to use address line 1 (A1) instead of address line 2 because when writing or reading the register value it reads or writes to bus address 0x0002. Fixing this for our case seems easy enough, just modify 0x0002 with 0x0004.

The question is how should we proceed? Should we rollback the added kernel options and add the modified driver as an external module? Or should we try to get the in-tree driver patched? The first option seems like would be easier and faster. The second option is totally uncharted territory for us.

Best regards,
Martin.

Hi @mmarcos.sensor ,

Jeremias is currently out of the office, so I’ll answer you in the meantime.

Regarding your question, I don’t think there’s a strictly correct answer to that, although I agree that the first option you suggested seems to be more adequate for your case, given that you would probably test the driver to see if it works for your hardware configuration.

Best regards,
Lucas Akira

We got it to work. We are going to test it thoroughly but it seems to be working fine for now.

We downloaded a 5.5.0 TorizonCore image (which is previous to the inclusion of the CONFIG_CAN_SJA1000=m and CONFIG_CAN_SJA1000_PLATFORM=m options), we download the SJA1000 kernel driver source code, modified as needed it and then added it to be compiled and included in our custom TorizonCore image. After a lot of trial and error and device tree tweaking we finally got it working.

As described in our previous post, for the driver, we only needed to modify a number in each of the sp_technologic_read_reg16 and sp_technologic_write_reg16 functions. We modified it so that in the second EIM access step instead of writing or reading to or from address reg_base + 2, it writes or reads to or from address reg_base + 4. Everything else is exactly the same as in the in-tree driver.

static u8 sp_technologic_read_reg16(const struct sja1000_priv *priv, int reg)
{
	struct technologic_priv *tp = priv->priv;
	unsigned long flags;
	u8 val;

	spin_lock_irqsave(&tp->io_lock, flags);
	iowrite16(reg, priv->reg_base + 0);
	val = ioread16(priv->reg_base + 4);
	spin_unlock_irqrestore(&tp->io_lock, flags);

	return val;
}

static void sp_technologic_write_reg16(const struct sja1000_priv *priv,
				       int reg, u8 val)
{
	struct technologic_priv *tp = priv->priv;
	unsigned long flags;

	spin_lock_irqsave(&tp->io_lock, flags);
	iowrite16(reg, priv->reg_base + 0);
	iowrite16(val, priv->reg_base + 4);
	spin_unlock_irqrestore(&tp->io_lock, flags);
}

The working weim device tree node is as follows:

&weim {
	status = "okay";

	/* weim memory map */
	ranges = <0 0 0x08000000 0x08000000>;

	/* SJA1000 CAN Controller on Colibri EIM_CS0 */
	can3@0,0 {
		compatible = "technologic,sja1000";
		reg = <0 0x00000000  0x00000100>;
		interrupt-parent = <&gpio3>;
		interrupts = <27 0x2>;
		nxp,external-clock-frequency = <24000000>;
		nxp,tx-output-config = <0x06>; /* TX0 push-pull */
		fsl,weim-cs-timing = <0x00810031 0x00001000 0x10040200
							  0x00000000 0x10FC01C0 0x00000000>;
		status = "okay";
	};
};

Register values for the fsl,weim-cs-timing property:

Expected signals timings are:

(We don’t use the ADV signal)

We also had to modify the pinctrl_weim_sram node to add the gpio interrupt line.

&pinctrl_weim_sram {
	fsl,pins = <
		MX6QDL_PAD_EIM_OE__EIM_OE_B		0xb0b1
		MX6QDL_PAD_EIM_RW__EIM_RW		0xb0b1
		/* Data */
		MX6QDL_PAD_CSI0_DATA_EN__EIM_DATA00	0x1b0b0
		MX6QDL_PAD_CSI0_VSYNC__EIM_DATA01	0x1b0b0
		MX6QDL_PAD_CSI0_DAT4__EIM_DATA02	0x1b0b0
		MX6QDL_PAD_CSI0_DAT5__EIM_DATA03	0x1b0b0
		MX6QDL_PAD_CSI0_DAT6__EIM_DATA04	0x1b0b0
		MX6QDL_PAD_CSI0_DAT7__EIM_DATA05	0x1b0b0
		MX6QDL_PAD_CSI0_DAT8__EIM_DATA06	0x1b0b0
		MX6QDL_PAD_CSI0_DAT9__EIM_DATA07	0x1b0b0
		MX6QDL_PAD_CSI0_DAT12__EIM_DATA08	0x1b0b0
		MX6QDL_PAD_CSI0_DAT13__EIM_DATA09	0x1b0b0
		MX6QDL_PAD_CSI0_DAT14__EIM_DATA10	0x1b0b0
		MX6QDL_PAD_CSI0_DAT15__EIM_DATA11	0x1b0b0
		MX6QDL_PAD_CSI0_DAT16__EIM_DATA12	0x1b0b0
		MX6QDL_PAD_CSI0_DAT17__EIM_DATA13	0x1b0b0
		MX6QDL_PAD_CSI0_DAT18__EIM_DATA14	0x1b0b0
		MX6QDL_PAD_CSI0_DAT19__EIM_DATA15	0x1b0b0
		/* Address */
		MX6QDL_PAD_EIM_DA15__EIM_AD15		0xb0b1
		MX6QDL_PAD_EIM_DA14__EIM_AD14		0xb0b1
		MX6QDL_PAD_EIM_DA13__EIM_AD13		0xb0b1
		MX6QDL_PAD_EIM_DA12__EIM_AD12		0xb0b1
		MX6QDL_PAD_EIM_DA11__EIM_AD11		0xb0b1
		MX6QDL_PAD_EIM_DA10__EIM_AD10		0xb0b1
		MX6QDL_PAD_EIM_DA9__EIM_AD09		0xb0b1
		MX6QDL_PAD_EIM_DA8__EIM_AD08		0xb0b1
		MX6QDL_PAD_EIM_DA7__EIM_AD07		0xb0b1
		MX6QDL_PAD_EIM_DA6__EIM_AD06		0xb0b1
		MX6QDL_PAD_EIM_DA5__EIM_AD05		0xb0b1
		MX6QDL_PAD_EIM_DA4__EIM_AD04		0xb0b1
		MX6QDL_PAD_EIM_DA3__EIM_AD03		0xb0b1
		MX6QDL_PAD_EIM_DA2__EIM_AD02		0xb0b1
		MX6QDL_PAD_EIM_DA1__EIM_AD01		0xb0b1
		MX6QDL_PAD_EIM_DA0__EIM_AD00		0xb0b1
		/* Interrupt */
		MX6QDL_PAD_EIM_D27__GPIO3_IO27		0xb0b1
	>;
};

The last test we did was replacing the TorizonCore v5.5.0 base image with a TorizonCore v5.7.0 development nightly build image (which includes CONFIG_CAN_SJA1000=m and CONFIG_CAN_SJA1000_PLATFORM=m options) for our custom image build. The custom image built successfully despite having both the in-tree SJA1000 driver and the external SJA1000 driver. We deployed the custom image to the target board and it seems like TorizonCore loaded the modified external driver instead of the in-tree driver, so CAN communications with the SJA1000 kept working.

torizon@colibri-imx6:~$ zcat /proc/config.gz | grep SJA
CONFIG_CAN_SJA1000=m
# CONFIG_CAN_SJA1000_ISA is not set
CONFIG_CAN_SJA1000_PLATFORM=m
torizon@colibri-imx6:~$ dmesg | grep sja
[   11.372921] sja1000: loading out-of-tree module taints kernel.
[   11.373804] sja1000 CAN netdevice driver
[   11.392842] sja1000_platform 8000000.can3: sja1000_platform device registered (reg_base=(ptrval), irq=165)
[  118.455981] sja1000_platform 8000000.can3 can0: setting BTR0=0x02 BTR1=0x1c
torizon@colibri-imx6:~$

Can we expect that out-tree driver will always be loaded instead of the in-tree driver? Or should we petition to remove the CONFIG_CAN_SJA1000_PLATFORM=m kernel option from TorizonCore build to avoid any future module conflicts?

Hi @mmarcos.sensor ,

Can we expect that out-tree driver will always be loaded instead of the in-tree driver?

I’m not sure this is necessarily always the case.

If I got this right, you loaded a custom SJA1000 driver based on the sja1000_platform in-tree one, also included as a module with CONFIG_CAN_SJA1000_PLATFORM=m.

So you have two loaded kernel modules named sja1000_platform: your custom one and the in-tree one.

In this case you have some options:

Best regards,
Lucas Akira