Введение

Привет, сегодня мы будем настраивать отправку данных с помощью CAN (Controller Area Network). В интернете много информации о том, как настроить CAN использую HAL библиотеку, а в случае использования CMSIS информация обрывочна, по этой причине решил рассказать о своем опыте работы.

Принцип работы CAN-сети в этой статье разбирать не будем т.к. на просторах интернета существует большой объем материла (особенно мне нравится как написано тут) на эту тему, но по ходу повествования будем останавливаться на некоторых нюансах.

Ставим себе задачу: заставить контроллер периодически отправлять кадры в CAN-сеть с скоростью передачи 250 кБит/с, со стандартной длиной идентификатора (11 бит) с полем данных размеров 8 байт.

Оборудование и ПО

Использовать будем микроконтроллер STM32F103C8T6 на отладочной плате, называемой в народе "Blue Pill" (рис. 1а). Также нам понадобится два приемопередачика (ПП) (по англицки "transceiver") для CAN-шины. Я использую 2 готовые платы с SN65HVD230 на борту (рис. 1б). На рис. 1в представлена схема этой платы . Для написания прошивки я буду использовать Keil uVision v5. Отладку и демонстрацию работы будем производить с помощью осциллографа и логического анализатора.

Рис. 1 – а) Отладочная плата Blue Pill; б) Плата ПП; в) Эл. схема платы ПП.
Рис. 1 – а) Отладочная плата Blue Pill; б) Плата ПП; в) Эл. схема платы ПП.

Соберем, не побоюсь этого слова, испытательный стенд. Соединяем выводы ПП с выводами Blue Pill:

CAN TX -> PA12

СAN RX -> PA11

Соединяем выводы ПП CAN_H и CAN_L между собой. Далее соединяем линии питания. В итоге должно получиться что-то подобное схеме на рис. 2. Желтый провод идет к входу логического анализатора.

Рис. 2 – Схема соединения блоков и фотография стенда
Рис. 2 – Схема соединения блоков и фотография стенда

Встраиваемое ПО

Напишем встраиваемое программное обеспечение для МК. Открываем Keil (или другую удобную для вас среду разработки: IAR, Eclipse/CubeIDE и др., главное, чтобы был установлен CMSIS), создаем проект и настраиваем для работы с нашей «Blue Pill». Если у Вас это вызывает затруднения, то в помощь статья.

Рис. 3 – Периферия, соединенная с шиной APB1
Рис. 3 – Периферия, соединенная с шиной APB1

Настроем систему тактирования. Смотрим в тех. спецификацию (datasheet) (рис. 3). Будем использовать высокоскоростной внутренний тактовый генератор (HSI). Причины почему именно его нет, да и это не является темой статьи. Главное, настроить так чтобы на шине APB1 была частота 36 МГц. Если все же интересно как настраивать систему тактирования, то можно ввести в поисковике «stm32 cmsis rcc», в интернете информации огромное количество, или почитать Reference Manual (бред, конечно, но вдруг поможет ????). Здесь я лишь приведу текст функции настройки тактирования с комментариями, она не идеальна, но на данном этапе с задачей справляется:

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;
}

Помещаем прототип функции в начало, само тело прячем в подвал, чтобы не мешалось. В функции main вызываем rcc_init().

Создадим функцию и uint8_t can_init(void). Далее текст в теле функции. Включим тактирование CAN:

RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;

CAN RX и TX по умолчанию находятся на выводах PA11 и PA12, соответственно (см. 31 стр. тех. спецификации). Включаем тактирование порта A:

RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

Настраиваем выводы на альтернативную функцию. Настроим сразу и прием (CAN Rx) , но в этой части он на не понадобится:

/* 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 */

Теперь нам нужен master control register (MCR) Переводим CAN в режим инициализации:

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

Отключим режим автоматической ретрансляции. Ретрансляция сообщения происходит при отсутствии подтверждения сообщения, а т.к. в нашей стенде нам принимать сообщение нечем, следовательно, подтвердить прием тоже некому. Кстати, забегая вперед, когда не приходит подтверждение получения сообщения то в CAN error status register (CAN_ESR) в битах 4…6 LEC[2:0]: (Last error code) возникает Acknowledgment Error. При этом поднимаются 4 и 5 бит (0b011).

CAN1->MCR |= CAN_MCR_NART;

Настроим чтобы CAN автоматически выходил из спящего режима (Automatic Wakeup Mode):

CAN1->MCR |= CAN_MCR_AWUM;

Настроим скорость передачи данных. Помним, мы поставили себе задачу, что скорость составляет 250 кБит/с, т.е длительность одного бита: T_bit = 1/(250*1000) = 4 мкс при этом частота шины APB1 f_APB1 = 36 МГц. Далее я приведу рисунок (рис. 4, взял тут), который пояснит некоторые термины и понятия которые будем использовать далее

Рис. 4 – Период импульсов для CAN
Рис. 4 – Период импульсов для CAN

В нашем случае: System Clock = f_APB1 = 36 МГц. CAN System Clock это частота после пред делителя. Теперь посмотрим на CAN Bit Period. Видим, что один БИТ состоит из 4 частей. Первая часть всегда длительностью 1 квант. Вторая и третья часть объединяются в первый сегмент. Далее идет точка «захвата» бита (Sample Point) и второй сегмент бита. Количество квантов в сегментах выбирается так чтобы точка «захвата» находилась в районе 87,5% длительности бита для протоколов CANopen и DeviceNet и 75% для ARINC 825. Наш вариант 87,5 %. Теперь у нас два пути: воспользоваться специальным калькулятором. либо составить и систему из двух простых уравнений и решить её. Пойдем по первому пути.

Вводим данные и нажимаем «Request Table». Ах да, еще есть величина ширина скачка синхронизации (SJW - Synchronization Jump Width) регулирует битовую синхронизацию по мере необходимости. На расчет она не влияет.

В результате получим таблицу:

Выбираем 2-ю строку исходя из положения точки «захвата» и получаем, что 1 сегменту у нас длиной 13 квантов, а 2-ой – 2 кванта времени, t_Q = 1/(16*250000) = 250 нс, а пред делитель равен 9. Более того, нам даже подсказывают, что нужно записать в регистр CAN_BTR, мы его рассмотрим чуть более детально. Кстати, можно было и не указывать желаемую скорость передачи, тогда бы нам калькулятор выдал значения для наиболее часто используемых скоростей.

Второй путь расчёта

Решаем задачку как в школе:

Дано:
Скорость передачи, bps = 250 кБит/с
Точка захвата на 87,5 % длины бита, sp = 0,875
Частота шины APB1, f_APB1 = 36 МГц

Решение:
Знаем, что длительность бита равна:
(x+y+1)*t_Q = 1/bps (1)
t_Q - длительность кванта времени
Считаем, что пред делитель равен 1 (PreSc = 1), тогда:
t_Q = PreSc/f_APB1 = 27,78 нс

Найти
Длительность 1 и 2 сегментов (x и y, соответственно)

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

Решаем систему ур-ий (1) и (2), получаем:

x = 125, y = 18, но если посмотреть в ref. manual, то можно заметить, что x (длительность 1-ого сегмента) может принимать значение от 1 до 16. В цикле увеличиваем значение пред делителя на 1 по порядку и решаем систему уравнений для каждого случая. Выбираем наиболее подходящий для нас вариант.

Ответ: PreSc = 9, x = 13, y = 2

Настроим скорость передачи, для этого пойдем в CAN bit timing register (CAN_BTR). Сбросим биты раздела Baud rate prescaler (BRP[9:0]) и установим: BRP[9:0] = PreSc - 1 = 8:

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

Сбросим биты, отвечающие за 1 временной сегмент и установим: TS1[3:0] = 13 – 1 = 12:

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

Сбросим биты, отвечающие за 2 временной сегмент и установим: TS2[2:0] = 2 – 1 = 1:

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

Биты 24…25 SJW[1:0] не трогаем их состояние т. о. получим что ширина скачка синхронизации t_SJW = 2t_Q = 500 нс.

Также в этом регистре можно настроить CAN в режим отладки (Loop back mode, Silent mode), но это не наш случай.

Настроим почтовый ящик номер 0 предназначенный для отправки (transmit mailbox 0). Чтобы добраться до него в структуре типа CAN_TypeDef есть вложенная структура sTxMailBox типа CAN_TxMailBox_TypeDef.  Сообщения, которые будем отправлять из этого ящика содержат данные (data frame), а не запрос (remote frame), сообщим об этом контроллеру:

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

Будем использовать стандартный идентификатор для кадра:

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

Отчистим идентификатор и поставим какой душе вздумается (в диапазоне от 0 до 3777):

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

Теперь сообщим сколько байт полезной информации будет передавать в одном кадре. CAN позволяет передать от 1 до 8. Эх, гулять так гулять, будем передавать 8. Определим это значение в начале файла, т.к. это значение нам еще пригодится в функции отправки сообщения:

#define DATA_LENGTH_CODE 8

и передадим это значение в регистр:

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

Все, с настройкой закончили. Естественно, дальше нужно будет настроить и прием, функцию будем допиливать, но это отдельная история. Нас пока интересует передача. Переходим из режима инициализации в «нормальный» режим (normal mode). Завершаем функцию. ссылка на полный код в конце статьи.

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

Теперь создаем функцию для отправки сообщения. Наша функция помимо того, что будет отправлять данные, будет делить их на кадры в соответствии с размером и проверять, возникли ли ошибки отправки. Передавать в функцию будем указатель на адрес начала передаваемых данных, и количество байт которое необходимо передать.

uint8_t can1_send(uint8_t * pData, uint8_t dataLength);

Нам понадобятся 2 счетчика i и j. Первый используем для перебора байтов, второй в случае, если размер кадра будет меньше количество передаваемых байт.

uint16_t i = 0;
uint8_t j = 0;

Проверим есть ли запросы на передачу для 0 почтового ящика, эта информация хранится в CAN transmit status register (CAN_TSR) а именно в 26 бите (TME0). Если есть, ждем, и если ждать придется слишком долго, то функция вернет 1.

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

Обнуляем данные, которые лежат в регистрах с данными чтобы не отправить случайно какой-нибудь мусор:

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

Далее пишем цикл, который постепенно заполняет каждый байт в регистрах данных TDLR и TDHR и делит данные на кадры если нужно передать больше байт чем настроено в DATA_LENGTH_CODE (мы настроили на 8 байт):

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++;
}

Далее добавляем запрос на передачу оставшихся данных и проверяем, что данные отправились без ошибок или возвращаем код ошибки (мы про этот регистр упоминали ранее):

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);

Теперь в main до бесконечного цикла вызываем функцию настройки can_init(), инициализируем тестовую строку и переменную для счетчика, который будет использоваться для задержки, а то поднимать таймеры в этой статье не будем, дабы не раздувать объем).

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

В цикле вызываем функцию и создаем простенькую задержку:

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

Все, наша программа готова.

Полный текст программы
#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 */
}

Испытания и результат

Cобираем проект, прошиваем, подключаем осциллограф и/или логический анализатор. Вот что показывает осциллограф:

Рис. 5 – Осциллограммы сигнала на CAN_H и CAN_L
Рис. 5 – Осциллограммы сигнала на CAN_H и CAN_L

На осциллограммах в первом случае отправляется 10 байт т.о. сообщение делится на 2 кадра. Во втором случае мы сообщение уменьшили до размера 8 байт – 1 кадр. И в третьем случаем мы длину кадра уменьшили до 4 байт, а длину сообщения вернули к 10 байтам – 3 кадра. Распознать вручную такую осциллограмму весьма затруднительно, для этого используем логически анализатор (я пользуюсь LWLA1034 в паре с ПО – PulseView) (рис. 6).

Рис. 6 – Сигналы, полученные логическим анализатором и интерпретированные PulseView.
Рис. 6 – Сигналы, полученные логическим анализатором и интерпретированные PulseView.

Видно, что данные отправляются как нужно (по таблице ASCII A = 0x41, B = 0x42 и т. д.), идентификатор какой записали (0x556), частота соответствует желаемой (250 кБит/с). Нет подтверждения, что отправленные данные получены, но ему и взяться неоткуда. Кстати посмотрим, что у нас записано в CAN error status register, а именно какой последний код ошибки. Зайдем в Keil в режиме отладки:

LEC = 3 – ошибка подтверждения как и ожидалось.

На этом всё! Задача выполнена, ура! Полный текст программы и проект в Keil можно скачать с github.

Если это будет кому-нибудь интересно, то напишу продолжение про настройку CAN через cmsis на прием.

Комментарии (19)


  1. Yoooriii
    29.12.2021 02:08

    Спасибо, интересно, как раз в тему, только начал ковыряться в КАН, а тут и статейка подоспела.


  1. belav
    29.12.2021 02:20
    +1

    Объясните, пожалуйста, для чего такое условие:

    if(i == 255) return 1;

    И из каких соображений выбрано число 255?


    1. laminar
      29.12.2021 07:23
      +1

      Максимальное значение для без знакового 8-ми битного. В цикле не указано крайнее значение, по этому если не использовать данное условие произойдет переполнение переменной и счет продолжится.


    1. Alekssandr
      29.12.2021 07:40

      Видимо чтоб цикл for не завис в бесконечности при не штатной ситуации.


    1. Gerrero
      29.12.2021 07:55

      1. что бы выскочить из цикла, когда i достигнет 255. (что бы не зависнуть)

      2. так как максимум uint8_t это 255.

      3. Почему именно 255? Думаю методом тыка. Если i = 255, значит точно что-то пошло не так и надо вылить отсюдава.


    1. MViktorE Автор
      29.12.2021 11:17

      Предыдущие коментарии все верно сказали:

      Это условие сделано на случай если что-то в инициализации RCC пойдет не так. Другой вопрос, что в основном теле программы не предусмотрены действия в случае возникновения ошибок. Но я для этого и сделал дополнение: "она не идеальна, но на данном этапе с задачей справляется". Если пытаться в одной статье объять всё, то она разбухнет до нечитабельных размеров.

      Почему именно 255?

      Мы выделили под переменную i 1 байт. 255 – это максимальное значение которое может быть у uint8_t (беззнаковой целочисленной переменной размером 1 байт)


      1. Amomum
        29.12.2021 12:44

        Существует стандартная именованная константа UINT8_MAX :)

        Возможно, если бы в коде была она, а не просто число 255, вопросов было бы чуть меньше (но не факт, конечно)


  1. laminar
    29.12.2021 08:05

    Вобще странный код на мой взгляд. Обычно для проверки готовности используют подобные конструкции:

    while(RCC->CR & RCC_CR_HSIRDY)

    Опять же описаная функция инициализации rcc имеет возвращаемое значение, которое никак не обрабатывается при вызове этой самой функции. Может конечно автор планировал сделать какое-то подобие обработчика ошибок?


    1. MViktorE Автор
      29.12.2021 11:31

      Обработчик ошибок не сделан с целью уменьшить объем кода и не уходить далеко от разбора переферии CAN.

      При такой постонавке:

      while(RCC->CR & RCC_CR_HSIRDY)

      программа наоборот не выйдет из цикла, а может быть даже не успеет туда зайти т.к. HSI не успеет поднять флаг Ready. Я бы переписал вот так:

      while(!(RCC->CR & RCC_CR_HSIRDY)) ...

      далее счетчик, и условие при котором заканчиваем цикл и выходим с кодом ошибки.

      но уверен есть еще более элегантные решения. В данной статье я решил не заострять на этом внимание.


      1. laminar
        29.12.2021 12:14
        +1

        Видимо поспешил, конечно же пока бит не установлен, кстати сам так пишу while(!(...)) но многие style guide предостерегают от подобного использования и рекомендуют прямое сравнение с 0

        while((RCC->CR & RCC_CR_HSIRDY) == 0);


  1. 8street
    29.12.2021 08:44
    +5

    Ох, ну зачем CMSIS? Не зря ST придумали CubeMX и HAL. Это сейчас понятно, что делают эти строчки:

    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;

    А через полгода надо будет каждую строчку разбирать с документацией. Кстати, что за кейворд "-&"? Оно скомпилируется вообще?


    1. 32bit_me
      29.12.2021 09:18

      При копипасте символы "<" и ">" превратились в &lt; и в &gt;, а " &" в &amp;, как они представлены в HTML То есть приведённый текст читается так:

      CAN1->BTR &= ~CAN_BTR_TS1;
      CAN1->BTR |= 12U << CAN_BTR_TS1_Pos;
      CAN1->BTR &= ~CAN_BTR_TS2;
      CAN1->BTR |= 1U << CAN_BTR_TS2_Pos;

      Такой текст скомпилируется.


    1. laminar
      29.12.2021 09:22
      +1

      Если работать с CMSIS чуть больше, чем настроить что-то по мануалу из интернетов пару раз, то достаточно быстро регистры запоминаются. Ну и естественно именуются они вменяемо BTR - bit time rigester т.е. эти строчки относятся к настройке таймингов. (в RM не подглядывал)


    1. MViktorE Автор
      29.12.2021 11:38

      У вас символы преобразуются некорректно. 32bit_me все верно написал.

      А CMSIS на мой взгляд полезен для понимания функционала переферии. Я не призываю везде его использовать, часто это не оптимальное решение. Но опыт работы с регистрами необходим если работаете с HAL, и замечу, что при работе с HAL можно работать и с CMSIS, часть HALа на нём базируется.


      1. 8street
        29.12.2021 14:10

        Это код из вашей статьи, стоит всё же его поправить.

        HAL реализован на некоторых МК с помощью CMSIS, но там хотя бы тестировано и переносимо. По факту, вы написали, что уже было написано. Для обучения — нормально, но для новых проектов, даже для пет-проектов, CMSIS не рекомендуется.

        Кстати, HAL позволяет абстрагироваться от тонкостей управления периферией. Нередко, чтобы что-то послать по шине, чтобы все работало, надо дернуть ещё вон тот регистр. С CMSIS это делает вручную. А ты вроде бы всё сделал, но забыл и куришь часами мануал, то ли клок забыл подать, то ли, где ещё какую настройку сделать.


        1. laminar
          29.12.2021 15:19

          но для новых проектов, даже для пет-проектов, CMSIS не рекомендуется.

          Можно где-то об этом прочитать? В моем представлении тот факт, что эта библиотека является стандартизованной, как раз должен говорить об обратном.


          1. 8street
            29.12.2021 16:54

            К сожалению, с ходу не нашел, где почитать. Считайте, что HAL это условный фреймворк, хотя ST называют его драйвером, поверх фреймворка (стандарта) CMSIS. И если CMSIS это вендоро независимый стандарт для описания МК, то HAL это кристалло-независимая библиотека ST. Если вы пишете под STM, то нецелесообразно применять только CMSIS. Кстати, если примяете HAL, то к регистрам так же можно обращаться с помощью CMSIS, но там это и не нужно.


            1. laminar
              29.12.2021 18:28

              К сожалению, с ходу не нашел, где почитать.

              Вот и сайт arm тоже не в курсе, что cmsis больше не рекомендуется.


  1. ovn83
    29.12.2021 17:18

    Делал CAN год назад на HAL. Была проблема, если PHY физически не подсоединён, в дуплексе, ошибка инициализации будет,