STM32, CMSIS, CAN, Part 1 – transmission


Introduction

Hi, today we will be setting up sending data using CAN (Controller Area Network). There is a lot of information on the Internet on how to configure CAN using the HAL library, and in the case of using CMSIS, the information is fragmentary, for this reason I decided to talk about my experience.

We will not analyze the principle of operation of the CAN network in this article, since there is a large amount of material (I especially like the way it is written here) on this topic, but in the course of the story we will dwell on some of the nuances.

We set ourselves a task: force the controller to periodically send frames to the CAN network with a transmission rate of 250 kBit / s, with a standard identifier length (11 bits) with a data field of 8 bytes.

Hardware and software

We will use the STM32F103C8T6 microcontroller on a debug board, popularly called “Blue Pill” (Fig. 1a). We also need two transceivers (PP) (in English “transceiver”) for the CAN bus. I am using 2 off-the-shelf boards with SN65HVD230 on board (Fig. 1b). In fig. 1c shows a diagram of this board. For writing the firmware I will be using Keil uVision v5. We will debug and demonstrate the work using an oscilloscope and a logic analyzer.

Rice.  1 - a) Blue Pill debug board;  b) PCB board;  c) Email  PCB circuit diagram.
Rice. 1 – a) Blue Pill debug board; b) PCB board; c) Email PCB circuit diagram.

Let’s put together, I’m not afraid of the word, a test bench. We connect the PP pins to the Blue Pill pins:

CAN TX -> PA12

CAN RX -> PA11

We connect the PP pins CAN_H and CAN_L to each other. Next, we connect the power lines. As a result, you should get something similar to the diagram in Fig. 2. The yellow wire goes to the logic analyzer input.

Rice.  2 - Block connection diagram and stand photo
Rice. 2 – Block connection diagram and stand photo

Embedded software

Let’s write embedded software for MK. Open Keil (or another development environment convenient for you: IAR, Eclipse / CubeIDE, etc., the main thing is that CMSIS is installed), create a project and configure it to work with our “Blue Pill”. If you have any difficulties, then the article will help you.

Rice.  3 - Peripherals connected to the APB1 bus
Rice. 3 – Peripherals connected to the APB1 bus

Let’s set up a clocking system. We look at those. specification (datasheet) (fig. 3). We will use a high speed internal clock generator (HSI). The reasons why it is not there, and this is not the topic of the article. The main thing is to configure so that the APB1 bus has a frequency of 36 MHz. If you are still interested in how to set up the clocking system, then you can enter “stm32 cmsis rcc” in the search engine, there is a huge amount of information on the Internet, or read the Reference Manual (nonsense, of course, but suddenly it will help 😊). Here I will only give the text of the clock setting function with comments, it is not ideal, but at this stage it copes with the task:

uint8_t rcc_init(void){
    /* Using the default HSI sorce - 8 MHz */
    /* Checking that the HSI is working */
    for (uint8_t i=0; ; i++){
        if(RCC->CR & (1<<RCC_CR_HSIRDY_Pos))
          break;
        if(i == 255)
          return 1;          
    }    
    /* RCC_CFGR Reset value: 0x0000 0000 */
    /* PLLSRC: PLLSRC: PLL entry clock source - Reset Value - 0 -> HSI oscillator clock / 2 selected as PLL input clock */
    /* HSI = 8 MHz */
    RCC->CFGR |= RCC_CFGR_PLLMULL9;     /* 0x000C0000 - PLL input clock*9 */
    RCC->CFGR |= RCC_CFGR_SW_1;         /* 0x00000002 - PLL selected as system clock */
    /* SYSCLK = 36 MHz */
    /*Also you can change another parameters:*/
    /* HPRE: AHB prescaler - Reset Value - 0 -> SYSCLK not divided */
    /* HCLK = 36 MHz (72 MHz MAX) */
    /* PPRE1: APB low-speed prescaler (APB1) - Reset Value - 0 -> 0xx: HCLK not divided */
    /* PPRE2: APB low-speed prescaler (APB2) - Reset Value - 0 -> 0xx: HCLK not divided */
    /* APB1 = APB2 = 36 MHz */
    /* ADCPRE: ADC prescaler - Reset Value - 0 -> PCLK2 divided by 2 */
    /* PLLXTPRE: HSE divider for PLL entry - ResVal - 0 -> HSE clock not divided */
    /* USBPRE: USB prescaler - ResVal - 0: PLL clock is divided by 1.5 */
    RCC->CR |=RCC_CR_PLLON;             /* 0x01000000 - PLL enable */
    for (uint8_t i=0; ; i++){
        if(RCC->CR & (1U<<RCC_CR_PLLRDY_Pos))
            break;
        if(i==255){
            RCC->CR &= ~(1U<<RCC_CR_PLLON_Pos);
            return 2;
        }
    }      
    return 0;
}

We place the prototype of the function at the beginning, and hide the body itself in the basement so that it doesn’t get in the way. In the main function, call rcc_init ().

Let’s create a function and uint8_t can_init (void). Next is the text in the body of the function. Let’s enable CAN clocking:

RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;

CAN RX and TX are by default located on pins PA11 and PA12, respectively (see page 31 of the technical specification). Turn on clocking of port A:

RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

We configure the conclusions for an alternative function. Let’s configure the reception (CAN Rx) right away, but in this part it won’t be needed:

/* PA11 - CAN_RX */
GPIOA->CRH	&= ~GPIO_CRH_CNF11;   /* CNF11 = 00 */ 
GPIOA->CRH	|= GPIO_CRH_CNF11_1;  /* CNF11 = 10 -> AF Out | Push-pull (CAN_RX) */
GPIOA->CRH 	|= GPIO_CRH_MODE11;   /* MODE8 = 11 -> Maximum output speed 50 MHz */
/* PA12 - CAN_TX */
GPIOA->CRH	&= ~GPIO_CRH_CNF12;	  /* CNF12 = 00 */
GPIOA->CRH	|= GPIO_CRH_CNF12_1;	/* CNF12 = 10 -> AF Out | Push-pull (CAN_TX) */
GPIOA->CRH 	|= GPIO_CRH_MODE12;   /* MODE8 = 11 -> Maximum output speed 50 MHz */

Now we need a master control register (MCR) We put CAN in initialization mode:

CAN1->MCR |= CAN_MCR_INRQ;              /* Initialization Request */

Disable the automatic relaying mode. Retransmission of a message occurs in the absence of confirmation of the message, and since in our stand we have nothing to receive the message, therefore, there is no one to confirm the reception either. By the way, looking ahead, when the confirmation of receipt of the message does not come, then in the CAN error status register (CAN_ESR) in bits 4 … 6 LEC[2:0]: (Last error code) Acknowledgment Error occurs. This raises 4 and 5 bits (0b011).

CAN1->MCR |= CAN_MCR_NART;

Let’s configure CAN to automatically wake up from sleep mode (Automatic Wakeup Mode):

CAN1->MCR |= CAN_MCR_AWUM;

Let’s adjust the baud rate. Remember, we set ourselves the task that the speed is 250 kBit / s, that is, the duration of one bit: T_bit = 1 / (250 * 1000) = 4 μs, while the APB1 bus frequency f_APB1 = 36 MHz. Next, I will give a picture (Fig. 4, took here), which will explain some of the terms and concepts that we will use below.

Rice.  4 - Pulse period for CAN
Rice. 4 – Pulse period for CAN

In our case: System Clock = f_APB1 = 36 MHz. CAN System Clock is the frequency after the prescaler. Now let’s look at CAN Bit Period. We see that one BIT consists of 4 parts. The first part is always 1 quantum long. The second and third parts are combined into the first segment. Next comes the Sample Point and the second segment of the bit. The number of quanta in the segments is chosen so that the “capture” point is in the region of 87.5% of the bit duration for the CANopen and DeviceNet protocols and 75% for the ARINC 825. Our option is 87.5%. Now we have two ways: use a special calculator… or compose a system of two simple equations and solve it. Let’s take the first path.

Enter the data and click on “Request Table”. Oh yeah, there is also the Synchronization Jump Width (SJW) value that adjusts the bit synchronization as needed. It does not affect the calculation.

As a result, we get a table:

We select the 2nd row based on the position of the “capture” point and we get that 1 segment is 13 quanta long, and the 2nd – 2 time quanta, t_Q = 1 / (16 * 250000) = 250 ns, and the divisor is 9. Moreover, we are even prompted what needs to be written to the CAN_BTR register, we will consider it in a little more detail. By the way, it was possible not to indicate the desired baud rate, then the calculator would give us values ​​for the most frequently used baud rates.

Second way of calculation

We solve the problem like in school:

Given:
Transfer rate, bps = 250 kbit / s
Snap point at 87.5% of bit length, sp = 0.875
APB1 bus frequency, f_APB1 = 36 MHz

Solution:
We know that the bit duration is:
(x + y + 1) * t_Q = 1 / bps (1)
t_Q – time slice duration
We assume that the limit is 1 (PreSc = 1), then:
t_Q = PreSc / f_APB1 = 27.78 ns

Find
Duration of 1 and 2 segments (x and y, respectively)

 frac {x + 1} {x + y + 1} = sp, (2)

We solve the system ur-s (1) and (2), we get:

x = 125, y = 18, but if you look at ref. manual, you can see that x (the duration of the 1st segment) can take a value from 1 to 16. In the loop, we increase the value of the limit by 1 in order and solve the system of equations for each case. We choose the most suitable option for us.

Answer: PreSc = 9, x = 13, y = 2

Let’s adjust the baud rate by going to the CAN bit timing register (CAN_BTR). Clear the Baud rate prescaler (BRP[9:0]) and install: BRP[9:0] = PreSc – 1 = 8:

CAN1->BTR &= ~CAN_BTR_BRP;
CAN1->BTR |= 8U << CAN_BTR_BRP_Pos;

Let’s clear the bits responsible for 1 time segment and set: TS1[3:0] = 13 – 1 = 12:

CAN1->BTR &= ~(0xFU << CAN_BTR_TS1_Pos);
CAN1->BTR |= 12U << CAN_BTR_TS1_Pos;

Let’s clear the bits responsible for the 2nd time segment and set: TS2[2:0] = 2 – 1 = 1:

CAN1->BTR &= ~(7U << CAN_BTR_TS2_Pos);
CAN1->BTR |=   1U << CAN_BTR_TS2_Pos;

Bits 24 … 25 SJW[1:0] do not touch their condition, so we get that the width of the synchronization jump is t_SJW = 2t_Q = 500 ns.

Also, in this register, you can configure CAN in debug mode (Loop back mode, Silent mode), but this is not our case.

Let’s configure the mailbox number 0 intended for sending (transmit mailbox 0). To get to it in the structure of the CAN_TypeDef type, there is a nested sTxMailBox structure of the CAN_TxMailBox_TypeDef type. Messages that we will send from this mailbox contain data (data frame), and not a request (remote frame), let us inform the controller about this:

CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_RTR;

We will use the standard identifier for the frame:

CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_IDE;

Let’s clean up the identifier and put it to whatever soul wants it (in the range from 0 to 3777):

CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_STID;
CAN1->sTxMailBox[0].TIR |= (0x556U << CAN_TI0R_STID_Pos);

Now let’s report how many bytes of useful information will be transmitted in one frame. CAN allows you to transfer from 1 to 8. Oh, walk like that, we will transfer 8. Let’s define this value at the beginning of the file, since This value will still be useful to us in the function of sending a message:

#define DATA_LENGTH_CODE 8

and pass this value to the register:

CAN1->sTxMailBox[0].TDTR &= ~CAN_TDT0R_DLC;
CAN1->sTxMailBox[0].TDTR |= (DATA_LENGTH_CODE << CAN_TDT0R_DLC_Pos);

All finished with the setup. Naturally, further it will be necessary to adjust the reception, we will finish the function, but this is another story. We are still interested in the transfer. We pass from the initialization mode to the “normal” mode (normal mode). We complete the function. link to the complete code at the end of the article.

CAN1->MCR &= ~CAN_MCR_INRQ;
return 0;
}

Now let’s create a function to send a message. Our function, in addition to sending data, will divide it into frames according to the size and check if there were any sending errors. We will transfer to the function a pointer to the address of the beginning of the transmitted data, and the number of bytes that must be transferred.

uint8_t can1_send(uint8_t * pData, uint8_t dataLength);

We need 2 counters i and j. The first is used to iterate over bytes, the second in case the frame size is less than the number of transmitted bytes.

uint16_t i = 0;
uint8_t j = 0;

Let’s check if there are any transmission requests for mailbox 0, this information is stored in the CAN transmit status register (CAN_TSR), namely in bit 26 (TME0). If there is, we are waiting, and if we have to wait too long, the function will return 1.

while (!(CAN1->TSR & CAN_TSR_TME0)){
    i++;
    if (i>0xEFFF) return 1;
}

We zero out the data that are in the registers with data so as not to accidentally send any garbage:

CAN1->sTxMailBox[mailboxNum].TDLR = 0;
CAN1->sTxMailBox[mailboxNum].TDHR = 0;

Next, we write a loop that gradually fills each byte in the TDLR and TDHR data registers and divides the data into frames if you need to transfer more bytes than configured in DATA_LENGTH_CODE (we set it to 8 bytes):

while (i<dataLength){
    if (i>(DATA_LENGTH_CODE-1)){
		    CAN1->sTxMailBox[0].TIR |= CAN_TI0R_TXRQ; /* Transmit Mailbox Request */
		    dataLength -= i;
		    j++;
		    while (!(CAN1->TSR & CAN_TSR_TME0)){      /* Transmit mailbox 0 empty? */
			      i++;
				if (i>0xEFFF) return 1;
		    }
		    if (CAN1->TSR & CAN_TSR_TXOK0){}          /* Tx OK? */
		    //else return ((CAN1->ESR & CAN_ESR_LEC)>>CAN_ESR_LEC_Pos); /* return Last error code */
		    i = 0;
		    CAN1->sTxMailBox[0].TDLR = 0;
		    CAN1->sTxMailBox[0].TDHR = 0;
    }
		if (i<4){
		    CAN1->sTxMailBox[0].TDLR |= (pData[i+j*DATA_LENGTH_CODE]*1U << (i*8));
		}
		else{
		    CAN1->sTxMailBox[0].TDHR |= (pData[i+j*DATA_LENGTH_CODE]*1U << (i*8-32));
		}
		i++;
}

Next, add a request to transfer the remaining data and check that the data was sent without errors or return an error code (we mentioned this register earlier):

CAN1->sTxMailBox[mailboxNum].TIR |= CAN_TI0R_TXRQ; /* Transmit Mailbox Request */
if (CAN1->TSR & CAN_TSR_TXOK0) return 0;
else return ((CAN1->ESR & CAN_ESR_LEC)>>CAN_ESR_LEC_Pos);

Now, in main, until an infinite loop, we call the can_init () configuration function, initialize the test line and the variable for the counter that will be used for delay, otherwise we will not raise the timers in this article so as not to inflate the volume).

uint16_t counter = 0;
uint8_t * data = “ABCDEFGHIJ9”;

In the loop, we call the function and create a simple delay:

can1_send(data, sizeof(data));
while(counter<0xFFFF)
    counter++;
counter = 0;

That’s it, our program is ready.

Full text of the program
#include "main.h"
#define DATA_LENGTH_CODE 8
uint8_t rcc_init(void);
uint8_t can_init(void);
uint8_t can_send(uint8_t * pData, uint8_t dataLength);
int main(void){
volatile uint16_t counter = 0;
uint8_t data[] = "ABCDEFGHIJ9";
rcc_init();
can_init();
while(1){
	can_send(data, sizeof(data));
	while(counter&lt;0xFFFF)
        counter++;
    counter = 0;
}

}
uint8_t rcc_init(void){
/* Using the default HSI sorce - 8 MHz /
/ Checking that the HSI is working */
for (uint8_t i=0; ; i++){
if(RCC->CR & (1<<RCC_CR_HSIRDY_Pos))
break;
if(i == 255)
return 1;
}
/* RCC_CFGR Reset value: 0x0000 0000 */
/* PLLSRC: PLLSRC: PLL entry clock source - Reset Value - 0 -&gt; HSI oscillator clock / 2 selected as PLL input clock */
/* HSI = 8 MHz */
RCC-&gt;CFGR |= RCC_CFGR_PLLMULL9;     /* 0x000C0000 - PLL input clock*9 */
RCC-&gt;CFGR |= RCC_CFGR_SW_1;         /* 0x00000002 - PLL selected as system clock */
/* SYSCLK = 36 MHz */
/*Also you can change another parameters:*/
/* HPRE: AHB prescaler - Reset Value - 0 -&gt; SYSCLK not divided */
/* HCLK = 36 MHz (72 MHz MAX) */
/* PPRE1: APB low-speed prescaler (APB1) - Reset Value - 0 -&gt; 0xx: HCLK not divided */
/* PPRE2: APB low-speed prescaler (APB2) - Reset Value - 0 -&gt; 0xx: HCLK not divided */
/* APB1 = APB2 = 36 MHz */
/* ADCPRE: ADC prescaler - Reset Value - 0 -&gt; PCLK2 divided by 2 */
/* PLLXTPRE: HSE divider for PLL entry - ResVal - 0 -&gt; HSE clock not divided */
/* USBPRE: USB prescaler - ResVal - 0: PLL clock is divided by 1.5 */
RCC-&gt;CR |=RCC_CR_PLLON;             /* 0x01000000 - PLL enable */
for (uint8_t i=0; ; i++){
  if(RCC-&gt;CR &amp; (1U&lt;&lt;RCC_CR_PLLRDY_Pos))
    break;
  if(i==255){
    RCC-&gt;CR &amp;= ~(1U&lt;&lt;RCC_CR_PLLON_Pos);
    return 2;
  }
}      
return 0;

}
uint8_t can_init(void){
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN; /* turn on clocking for CAN /
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; / turn on clocking for GPIOA /
/ PA11 - CAN_RX /
GPIOA->CRH	&= ~GPIO_CRH_CNF11;     / CNF11 = 00 /
GPIOA->CRH	|= GPIO_CRH_CNF11_1;	/ CNF11 = 10 -> AF Out | Push-pull (CAN_RX) /
GPIOA->CRH 	|= GPIO_CRH_MODE11;     / MODE11 = 11 -> Maximum output speed 50 MHz /
/ PA12 - CAN_TX /
GPIOA->CRH	&= ~GPIO_CRH_CNF12;	    / CNF12 = 00 /
GPIOA->CRH	|= GPIO_CRH_CNF12_1;	/ CNF12 = 10 -> AF Out | Push-pull (CAN_TX) /
GPIOA->CRH 	|= GPIO_CRH_MODE12;     / MODE12 = 11 -> Maximum output speed 50 MHz */
CAN1-&gt;MCR |= CAN_MCR_INRQ;          /* Initialization Request */
CAN1-&gt;MCR |= CAN_MCR_NART;          /* Not autoretranslate transmission */
CAN1-&gt;MCR |= CAN_MCR_AWUM;          /* Automatic Wakeup Mode */
/* clean and set Prescaler = 9 */
CAN1-&gt;BTR &amp;= ~CAN_BTR_BRP;          
CAN1-&gt;BTR |= 8U &lt;&lt; CAN_BTR_BRP_Pos;
/* clean and set T_1s = 13, T_2s = 2 */
CAN1-&gt;BTR &amp;= ~CAN_BTR_TS1;
CAN1-&gt;BTR |= 12U &lt;&lt; CAN_BTR_TS1_Pos;
CAN1-&gt;BTR &amp;= ~CAN_BTR_TS2;
CAN1-&gt;BTR |=   1U &lt;&lt; CAN_BTR_TS2_Pos;

CAN1-&gt;sTxMailBox[0].TIR &amp;= ~CAN_TI0R_RTR;                    /* data frame */
CAN1-&gt;sTxMailBox[0].TIR &amp;= ~CAN_TI0R_IDE;                    /* standart ID */ 
CAN1-&gt;sTxMailBox[0].TIR &amp;= ~CAN_TI0R_STID;
CAN1-&gt;sTxMailBox[0].TIR |= (0x556U &lt;&lt; CAN_TI0R_STID_Pos);
CAN1-&gt;sTxMailBox[0].TDTR &amp;= ~CAN_TDT0R_DLC;                  /* length of data in frame */
CAN1-&gt;sTxMailBox[0].TDTR |= (DATA_LENGTH_CODE &lt;&lt; CAN_TDT0R_DLC_Pos);
CAN1-&gt;MCR &amp;= ~CAN_MCR_INRQ;                                  /* go to normal mode */ 
return 0;	

}
uint8_t can_send(uint8_t * pData, uint8_t dataLength){
uint16_t i = 0;
uint8_t j = 0;
while (!(CAN1->TSR & CAN_TSR_TME0)){
i++;
if (i>0xEFFF) return 1;
}
i = 0;
CAN1->sTxMailBox[0].TDLR = 0;
CAN1->sTxMailBox[0].TDHR = 0;
while (i<dataLength){
if (i>(DATA_LENGTH_CODE-1)){
CAN1->sTxMailBox[0].TIR |= CAN_TI0R_TXRQ;                 /* Transmit Mailbox Request /
dataLength -= i;
j++;
while (!(CAN1->TSR & CAN_TSR_TME0)){                      / Transmit mailbox 0 empty? /
i++;
if (i>0xEFFF) return 1;
}
if (CAN1->TSR & CAN_TSR_TXOK0){}                          / Tx OK? /
//else return ((CAN1->ESR & CAN_ESR_LEC)>>CAN_ESR_LEC_Pos); / return Last error code /
i = 0;
CAN1->sTxMailBox[0].TDLR = 0;
CAN1->sTxMailBox[0].TDHR = 0;
}
if (i<4){
CAN1->sTxMailBox[0].TDLR |= (pData[i+jDATA_LENGTH_CODE]1U << (i8));
}
else{
CAN1->sTxMailBox[0].TDHR |= (pData[i+jDATA_LENGTH_CODE]1U << (i8-32));
}
i++;
}
CAN1->sTxMailBox[0].TIR |= CAN_TI0R_TXRQ; / Transmit Mailbox Request /
if (CAN1->TSR & CAN_TSR_TXOK0) return 0;
else return ((CAN1->ESR & CAN_ESR_LEC)>>CAN_ESR_LEC_Pos); / return Last error code */
}

Test and result

Putting together a project, flashing it, connecting an oscilloscope and / or logic analyzer. This is what the oscilloscope shows:

Rice.  5 - Oscillograms of the signal on CAN_H and CAN_L
Rice. 5 – Oscillograms of the signal on CAN_H and CAN_L

On oscillograms, in the first case, 10 bytes are sent, thus. the message is divided into 2 frames. In the second case, we reduced the message to 8 bytes – 1 frame. And in the third case, we reduced the frame length to 4 bytes, and returned the message length to 10 bytes – 3 frames. It is very difficult to recognize such an oscillogram manually, for this we use a logic analyzer (I use LWLA1034 in tandem with PulseView software) (Fig. 6).

Rice.  6 - Signals received by the logic analyzer and interpreted by PulseView.
Rice. 6 – Signals received by the logic analyzer and interpreted by PulseView.

It can be seen that the data is sent as needed (according to the ASCII table A = 0x41, B = 0x42, etc.), the identifier of which was recorded (0x556), the frequency corresponds to the desired one (250 kbit / s). There is no confirmation that the sent data has been received, but he has nowhere to come from. By the way, let’s see what we have written in the CAN error status register, namely, what is the last error code. Let’s go to Keil in debug mode:

LEC = 3 – Confirmation error as expected.

That’s all! The task is completed, hurray! The full text of the program and the project in Keil can be downloaded from github

If anyone is interested in this, I will write a sequel about configuring CAN via cmsis for reception.