
Многие STM32 микроконтроллеры обладают CAN MAC. Даже не одним. В этом тексте я написал про особенноcти работы c CAN на STM32.
Что такое CAN и зачем он нужен можете почитать тут (CAN-шина (Теория)).
Постановка задачи
Научиться управлять CAN MACом на микроконтроллере семейства STM32F4x. Научиться отправлять, принимать CAN пакеты на STM32F4. Научиться настраивать аппаратную фильтрацию принимаемых пакетов.
Аппаратная часть
Существует широко известная, доступная в продаже отладочная учебно-тренировочная электронная плата JZ-F407VET6 на основе микроконтроллера STM32F407VET6. На PCB как раз STM32 два CAN, USB и светодиоды. JZ-F407VET6 плата идеально подходит для изучения CAN. Притом стоит вокруг да около 2kRUR.

Отлаживаться можно USB-CAN переходником USB2CANFD_V1.

Практическая часть
Обычно CAN выводят на разъём DB-9. CAN_L - 2 пин, CAN_H - 7 пин.

Что мы знаем про CAN в STM32F4x? Там обычно мегабитный CAN MAC стандарта 2.0 B. Может работать с расширенными идентификаторами. Обладает механизмом фильтрации ID. Есть FIFO на прием. Три почтовых ящика для отправки.
CAN | MAC addr | Bus | TxPad | RxPad |
CAN1 | 0x4000_6400 | APB1 | PB9 | PB8 |
CAN2 | 0x4000_6800 | APB1 | PB6 | PB5 |
Пины могут быть такие
GPIO | PinMux | CAN |
PA11 | 9 | CAN1_RX |
PD0 | 9 | CAN1_RX |
PB8 | 9 | CAN1_RX |
PA12 | 9 | CAN1_TX |
PD1 | 9 | CAN1_TX |
PB9 | 9 | CAN1_TX |
Алгоритм инициализации CAN MAC-a
Вот основные настройки CAN MAC
--Подать тактирование на GPIO пины, на которых заложен CAN. (CLK)
--Отключать CAN-периферию от шины на время инициализации.*
--Включить прерывания глобально (Core CMSIS)
--Подать тактирование на CAN MAC
--Настроить битовую скорость работы MAC ( CAN_BTR )
--Включить CAN прерывания ( CAN_IER )
--Задать приоритет CAN прерываниям (NVIC)
--Настроить CAN фильтры
--Определить поведение по отношению к bus-off: восстанавливаться автоматически или нет ( ABOM )
--Включить автоматическую ретрансляцию пакета ( NART )
--Определить приоритет отправки почтовых ящиков ( TXFP )
--Определить запрет (или его отмену) на прием в случае переполнения FIFO. ( RFLM )
--Переключить GPIO пины на CAN MAC (GPIO)
Настройка битовой частоты
Все два CAN MAC посажены на системную шину APB1. Максимальная частота тактирования 42MHz.

CAN MAC очень чувствителен к битовой скорости. В STM32 битовая скорость задается системой уравнений записанной на рисунке 346 из основного даташита RM (RM0090 Reference manual). Фаза propagation вообще отсутствует. Как можно заметить, в регистры пишутся не реальные математические значения сегментов CAN бита, а значения на единицу меньше. Поэтому регистры надо потом уметь корректно интерпретировать в реальные физические значения. Пока качественно понятно что, если увеличивать предделитель CAN (BTR.BRP), то битовая частота CAN уменьшится. Если уменьшать предделитель CAN, то битовая частота CAN увеличивается.
![В спеке ошибка. Первое слагаемое это не 1*tq, а tq * (SJW[3:0] + 1); В спеке ошибка. Первое слагаемое это не 1*tq, а tq * (SJW[3:0] + 1);](https://habrastorage.org/r/w1560/getpro/habr/upload_files/3fa/442/ef1/3fa442ef14d25809ac55cffa7a2179b4.png)
В Си коде это выглядит так.
/* see Figure 346. Bit timing */
bool CAN_baudrate_calc(const uint32_t base_clock_hz,
const CanReg_BTR_t BTR,
uint32_t* const bit_rate) {
bool res = false;
if(base_clock_hz) {
if(bit_rate) {
float t_base_period_s = 1.0 / ((float)base_clock_hz);
uint32_t baud_rate_prescaler = BTR.BRP + 1;
float tq = ((float)baud_rate_prescaler) * t_base_period_s;
float sync = tq * (BTR.SJW + 1);
float t_bs1 = tq * (BTR.TS1 + 1);
float t_bs2 = tq * (BTR.TS2 + 1);
float bit_time_s = sync + t_bs1 + t_bs2;
float bit_rate_hz = 1.0 / bit_time_s;
*bit_rate = (uint32_t)bit_rate_hz;
res = true;
}// end of if(bit_rate)
}// end of if(base_clock_hz)
return res;
}// end of CAN_baudrate_calc()В качестве переменных задействованы следующие битовые поля
Переменная | Разрядность, bit | Назначение | Возможные значения |
TS1 | 4 | Time segment 1 | 0....15 |
TS2 | 3 | Time segment 2 | 0....7 |
BRP | 10 | Baud rate prescaler | 0.....1023 |
SJW | 2 | Resynchronization jump width | 0....3 |
Эти переменные прописываются в регистре CAN_BTR

Вот программа для вычисления значений регистра CAN_BTR. Алгоритм перебором находит нужные значения. Фактически тут происходит решение Диофантового уравнения.
/*see Figure 346. Bit timing*/
bool CAN_baudrate_get(uint8_t num,
uint32_t* const bit_rate) {
bool res = false;
const CanInfo_t* Info = CanGetInfo(num);
if(Info) {
CanReg_BTR_t CAN_BTR;
CAN_BTR.dword = Info->CANx->BTR;
uint32_t base_clock_hz = CAN_base_clock_get(num);
res = CAN_baudrate_calc(base_clock_hz, CAN_BTR, bit_rate);
return res;
}
return res;
}
/* Figure 346. Bit timing
*/
static bool CAN_stm32_segment_info_calc(uint32_t bus_freq_hz,
const uint32_t bit_rate_hz,
CanSegmentInfo_t* const Segment) {
bool res = false;
LOG_INFO(CAN, "busFreq:%u Hz,BitRate:%u Bit/s", bus_freq_hz, bit_rate_hz);
if(Segment) {
float tick_period_s = 1.0f / ((float)bus_freq_hz);
LOG_INFO(CAN, "tick_period:%s s", FloatBigToStr(tick_period_s));
CanBestBtr_t BestBtr;
BestBtr.error = INT_MAX;
uint32_t b, t1, t2;
for(b = 0; b <= 1023; b++) {
for(t1 = 0; t1 <= 14; t1++) {
for(t2 = 0; t2 <= 6; t2++) {
CanReg_BTR_t CAN_BTR = {0};
CAN_BTR.BRP = b;
CAN_BTR.TS1 = t1;
CAN_BTR.TS2 = t2;
CAN_BTR.SJW = 0;
uint32_t bit_rate_get_hz = 0;
res = CAN_baudrate_calc(bus_freq_hz, CAN_BTR,
&bit_rate_get_hz);
if(res) {
int32_t diff_hz = ((int32_t)bit_rate_hz) -
((int32_t)bit_rate_get_hz);
int32_t abs_diff_hz = abs(diff_hz);
if(abs_diff_hz < BestBtr.error) {
BestBtr.error = abs_diff_hz;
BestBtr.CAN_BTR.dword = CAN_BTR.dword;
res = true;
if(0 == abs_diff_hz) {
LOG_INFO(CAN, "SpotSol");
CanDiagRegBTR(CAN_BTR.dword);
}
}
}
}
}
}
res = CanCAN_BTRToSegment(&BestBtr.CAN_BTR, Segment);
LOG_INFO(CAN, "SpotSeg[%s]", CanSegmentInfoToStr(Segment));
}
return res;
}Чтобы проверить правильность установки битовой скорости, следует установить щуп осциллографа на пин CAN_TX и попросить прошивку отправлять пакеты c полезными данными 0x5555555555555555. Вам могут пригодиться прищепки SDK08. Это специальный тестировочный код (как и 0xAAAAAA...AA). Например, если вы настроили MAC на 500kBit/s, то на осциллографе вы должны увидеть частоту 250kHz. То есть в одном периоде сигнала 0xAAAAAAAA вложено два бита данных. Если так и есть, значит настройки частоты применились правильно.
BitRate,kBit/s | частота 0x555555, kHz | Длительность бита, us |
100 | 50 | 10 |
250 | 125 | 4 |
500 | 250 | 2 |
1000 | 500 | 1 |
Алгоритм отправки CAN пакета
Чтобы отправить пакет, надо просто аккуратно заполнить TxMailBox. Далее всё сделается само собой.
--Активировать прерывание по окончанию отправки. В регистре CAN interrupt enable register (CAN_IER) прописать нулевой бит TMEIE=1 ( Transmit mailbox empty interrupt enable ). Установка бита RQCPx спровоцирует прерывание.
--Выбрать пустой mailbox. Их всего три.
--заполнить значение ID ( CAN_TIxR )
--заполнить тип ID: стандартный или расширенный ( CAN_TIxR )
--заполнить тип пакета: data remote ( CAN_TIxR )
--заполнить размер payload ( CAN_TDTxR DLC)
--заполнить байты данных ( CAN_TDLxR CAN_TDHxR )
--Дать отмашку на отправку пакета. Взвести бит TXRQ=1 в регистре CAN_TIxR.

После этого всё остальное сделает за Вас аппаратура CAN-MAC. Он посчитает CRC15, выполнит арбитраж, отправит пакет в шину и укажет об окончании процесса выстреливанием бита RQCP0 (RequestDone) отправки в регистре (CAN_TSR) + прерыванием.
Всегда и везде отправка намного проще, чем прием. Чтобы инициировать внеплановую отправку CAN тестировочного пакета я открываю UART-CLI и просто набираю строку cs 1 0x55 0x5555555555555555

Отлаживаться можно переходником USB2CANFD_V1. Видно, что CanGaroo корректно принимает отправленные микроконтроллером CAN пакеты.

Алгоритм приема CAN пакета
Ес��ь два способа: настроить прерывания или опрашивать регистры. Но в целом они оба делают одно и то же.
--Сконфигурировать GPIO пины на альтернативную функцию работы с CAN MAC
--Настроить разрешение глобальных прерываний
--Активировать CAN прерывания в настройках NVIC
--Подать тактирование на CAN MAC
--Определить приоритет CAN прерывания
--Разрешить прерывания в настройках CAN MAC [CAN interrupt enable register (CAN_IER)]. Надо установить биты FMPIE0 FFIE0 FOVIE0 FMPIE1 FFIE1 FOVIE1.

И вот сработало CAN прерывание.
--Надо допросить регистр CAN_RFхR. Если там счетчик FMP показывает больше 0 (очередь не пуста), то можно извлечь пакет из аппаратной очереди приема.
--Вызывать CallBack приема пакета.
--Определить тип пакета, лежащий в очереди (CAN_RIxR)
--Прочитать ID пакета (CAN_RIxR)
--Пpочитать размер принятого payload (CAN_RDTxR)
--Прочитать номер фильтра, который принял этот пакет (CAN_RDTxR)
--Прочитать Timestamp (CAN_RDTxR) [не работает ]
--Прочитать байты данных ( CAN_RDLxR CAN_RDHxR )
--Очистить верхушку FIFO. Прописать RFOM0=1 в регистр CAN_RF0R или CAN_RF1R
--Принятый пакет поместить в очередь принятых пакетов

Обработка CAN MAC
Драйвер CAN MAC это не просто функция init, функция отправки и обработчик прерываний. Надо еще периодически следить за здоровьем самого MAC . Существует море причин, по которым CAN MAC может взять и заклинить.

Надо постоянно в суперцикле опрашивать статусные регистры CAN и, если там возникли ошибки, то как минимум сразу же писать про это в UART-CLI. Могут возникнуть bus-off, может переполниться входная аппаратная очередь, может увеличиться счетчик TEC или REC. Надо вовремя выявлять эти сбои, корректно и оперативно реагировать на них. Надо анализировать все read-only поля в регистрах.
Регистр | Название регистра | Смещение |
CAN_MSR | CAN master status register | 0x04 |
CAN_TSR | CAN transmit status register | 0x08 |
CAN_RF0R | CAN receive FIFO 0 register | 0x0C |
CAN_RF1R | CAN receive FIFO 1 register | 0x10 |
CAN_ESR | CAN error status register | 0x18 |
CAN_RI0R | CAN receive FIFO mailbox identifier register | 0x1B0 |
CAN_RI1R | CAN receive FIFO mailbox identifier register | 0x1C0 |
CAN_RDT0R | CAN receive FIFO mailbox data length control and time stamp register | 0x1B4 |
CAN_RDT1R | CAN receive FIFO mailbox data length control and time stamp register | 0x1C4 |
CAN_RDL0R | CAN receive FIFO mailbox data low register | 0x1B8 |
CAN_RDL1R | CAN receive FIFO mailbox data low register | 0x1C8 |
CAN_RDH0R | CAN receive FIFO mailbox data high register | 0x1BC |
CAN_RDH1R | CAN receive FIFO mailbox data high register | 0x1CC |
Скорее всего придется прямо на лету что-то отремонтировать.
Индикация приема и отправки
Хорошей практикой считается, когда драйвер CAN LED-ами информирует человека о направлении движения трафика. Обычно ставят два LEDа: на Tx и на Rx. При этом CAN - это высокоскоростной интерфейс. Пакеты могут приходить каждую миллисекунду. Если просто мигать при отправке (или приеме), то получится одно сплошное свечение. Со стороны можно будет ошибочно заключить, что прошивка вообще зависла, заклинила. Чтобы это никого не дезориентировало, драйвер CAN должен управлять не просто GPIO драйвером, а полноценным программным LED драйвером, который будет генерировать программным PWM заметные для человека мигания (например 5-15 Hz) по команде от CAN драйвера. Пустить своего рода поезд из импульсов.
Аппаратная Фильтрация ID
Чтобы слишком часто не прерывать процессор прерываниями по CAN можно настроить игнорирование конкретных значения ID. Это позволит разгрузить процессор от рутинной работы и увеличить быстродействие всей прошивки в целом. Называется это фильтрацией. В STM32 заложено 28 фильтров. Каждый экземпляр фильтра состоит из двух регистров: CAN_FxR0 и CAN_FxR1, где х может принимать значения от 0 до 27 включительно.
Сконфигурировать фильтр можно вот этой функцией.
typedef union {
uint32_t dword;
struct {
uint32_t ZERO:1; /*0-need*/
uint32_t RTR:1; /*0-need*/
uint32_t IDE:1; /*1-Ext ID 0-StdID*/
uint32_t RES1:29;
};
struct {
uint32_t RES2:3;
uint32_t EXT_ID:29;
};
struct {
uint32_t RES3:21;
uint32_t STD_ID:11;
};
} CAN_RegFilter32Bit_t;
/*
bus 1 2
format CAN_FRAME_ID__STD CAN_FRAME_ID__EXT
filt_num 0...13*/
bool CAN_filter_id_mask_set(
const uint8_t bus_num,
const uint8_t filt_num,
const CanIdentifier_t format,
const uint32_t filt_id,
const uint32_t filt_mask) {
bool res = false;
CanHandle_t *Node = CanGetNode(bus_num);
if (Node) {
CAN_FilterTypeDef xStmHalFilter = { 0 };
xStmHalFilter.FilterMode = CAN_FILTERMODE_IDMASK;
xStmHalFilter.FilterScale = CAN_FILTERSCALE_32BIT;
xStmHalFilter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
xStmHalFilter.FilterActivation = ENABLE;
xStmHalFilter.SlaveStartFilterBank = CAN2_FILTER_NUM_FIRST;
CAN_RegFilter32Bit_t xFilterID;
CAN_RegFilter32Bit_t xFilterMask;
xFilterID.ZERO = 0;
xFilterID.RTR = 0;
xFilterID.IDE = CAN_IdTypeToFiltIDE(format);
xFilterMask.ZERO = 0;
xFilterMask.RTR = 0;
xFilterMask.IDE = CAN_IdTypeToFiltIDE(format);
switch (format) {
case CAN_FRAME_ID__STD: {
xFilterID.STD_ID = CAN_STD_ID_MAX_VAL & filt_id;
xFilterMask.STD_ID = filt_mask;
} break;
case CAN_FRAME_ID__EXT: {
xFilterID.EXT_ID = CAN_EXT_ID_MAX_VAL & filt_id;
xFilterMask.EXT_ID = filt_mask;
} break;
default:
res = false;
break;
}
T32Union_t Un32ID;
Un32ID.u32 = xFilterID.dword;
xStmHalFilter.FilterIdHigh = (uint16_t) Un32ID.u16[1];
xStmHalFilter.FilterIdLow = (uint16_t) Un32ID.u16[0];
T32Union_t Un32Mask;
Un32Mask.u32 = xFilterMask.dword;
xStmHalFilter.xFilterMaskIdHigh = (uint16_t) Un32Mask.u16[1];
xStmHalFilter.xFilterMaskIdLow = (uint16_t) Un32Mask.u16[0];
int8_t filter_num = CAN_filter_num_calc(bus_num, filt_num) ;
if(filter_num>0) {
HAL_StatusTypeDef ret = HAL_ERROR;
xStmHalFilter.FilterBank = (uint32_t) filter_num;
ret = HAL_CAN_ConfigFilter(&Node->Handle, &xStmHalFilter);
res = StmHALretToRes(ret);
}
}
return res;
}Каждый отдельный фильтр можно активировать или отключить, прописав соответствующий бит в регистре CAN_FA1R.

Даже если фильтры запрещают ID регистр CAN_MSR все равно показывает Receive mode. Так MCU может судить о том, что в шине что-то всё-таки происходит.
API CAN драйвера
Минималистический API для драйвера CAN MAC может выглядеть как-то так
#ifndef CAN_DRV_H
#define CAN_DRV_H
#include "can_drv_types.h"
#include "can_drv_config.h"
CanHandle_t* CanGetNode(uint8_t num);
const CanConfig_t* CanGetConfig(uint8_t num);
bool CAN_is_message_valid(const CanMessage_t* const Message);
/* API */
bool CAN_init(void);
bool CAN_init_one(uint8_t num);
bool CanIsValidConfig(const CanConfig_t* const Config);
bool CAN_segment_info_calc(uint32_t bus_freq_hz, uint32_t baud_rate_hz, CanSegmentInfo_t* const SegmentInfo);
bool CAN_wait_tx_done_ll(CanHandle_t* const Node);
bool CAN_is_valid_segment( const CanSegmentInfo_t* const Segment);
uint32_t CAN_time_seg_to_bit_rate(const uint32_t clk_freq_hz, const CanSegmentInfo_t* const Segment);
uint32_t CAN_segment_to_main_pre_scaler(const CanSegmentInfo_t* const Segment);
bool CAN_proc(void);
bool CAN_proc_one(uint8_t num);
/* setter */
bool CAN_transmit_message(uint8_t num, const CanMessage_t* const Message);
bool CAN_baudrate_set(const uint8_t num, const uint32_t baudrate) ;
bool CAN_filter_allow_id(uint8_t num, const uint32_t id);
bool CAN_filter_ban_id(uint8_t num, const uint32_t id);
bool CAN_interrupt_ctrl(uint8_t num, bool on_off);
bool CAN_loopback_set(const uint8_t num, const bool on_off);
bool CAN_phy_disconnect(uint8_t num);
bool CAN_phy_connect(uint8_t num);
/* getter */
bool CAN_segment_to_baudrate(uint32_t base_clock_hz, CanSegmentInfo_t* const Segment, uint32_t* const bit_rate);
bool CAN_filter_is_active(const uint8_t num, const uint8_t filter_num);
bool CAN_loopback_get(const uint8_t num, bool* const on_off);
bool CAN_baudrate_get(uint8_t num, uint32_t * const bit_rate);
bool CAN_is_my_id(const uint8_t num, const uint32_t can_id);
bool CAN_is_ext_id(const uint32_t id);
bool CAN_is_std_id(const uint32_t id);
bool CAN_rec_get(uint8_t num, uint32_t* const rec);
bool CAN_tec_get(uint8_t num, uint32_t* const tec);
bool CAN_segments_get(uint8_t num, CanSegmentInfo_t* const SegmentInfo);
CanClockSource_t CAN_get_clk_src(const uint8_t num);
CanMacMode_t CAN_get_mac_mode(const uint8_t num);
CanIdentifier_t CAN_id_val_to_id_type(const uint32_t id_val);
float CAN_time_quanta_get(uint8_t num) ;
uint32_t CAN_base_clock_get(const uint8_t num);
uint8_t CAN_id_type_to_bit_len(const CanIdentifier_t id_type);
#endif /* CAN_DRV_H */Достоинства
++ Относительная простота. В сравнении с современными китайскими автомобильными микроконтроллерами, STM-овский bxCAN это просто детская игрушка.
Недостатки
--Не работает механизм CAN-овских тайм штампов. Это даже честно прописано в Errata sheet. Поэтому тайм штампы придется брать из аппаратного таймера: (TIM2 или TIM5). Они как раз 32хбитные. Либо делать каскадный таймер из двух 16-бит таймеров.

--Нет поддержки CAN-FD. Максимальная скорость 1 MBit/s
--Короткий TxFIFO. Всего на три пакета.
--Нет фильтра на исключение отдельного ID. Есть только фильтры на совпадение и фильтры на ID и маску.
--Свободное место в почтовом ящике на отправку - это еще не гарантия, что пакет будет в самом деле отправлен. CAN шина может быть просто занята другими устройствами. Это как стоять в пробке при выезде из двора. Аппаратное обеспечение указывает на успешную передачу, устанавливая биты RQCP (Request completed) и TXOK (Transmission OK ) в регистре CAN_TSR (CAN transmit status register.
--Надо отключать CAN-периферию от шины на время инициализации. Буквально переводить CAN_RX и CAN_TX на GPIO вход. Иначе внешний обильный трафик на шине может препятствовать нормальному ходу алгоритма инициализации и функция HAL_CAN_Init у вас свалится в timeout или error. При установке INRQ=1 CAN контроллер может просто не перейти в режим инициализации.
“ Once software has set the INRQ bit, the CAN hardware waits until the current CAN activity (transmission or reception) is completed before entering the initialization mode. Hardware signals this event by setting the INAK bit in the CAN_MSR register.”
Результат
Удалось научиться отправлять и принимать CAN пакеты на STM32. Удалось разобраться в CAN фильтра��.
Буду признателен, если поможете найти мне ответы на вопросы по CAN в конце текста.
Больше ссылок
Название | URL |
CAN-шина и stm32 / STM32 / stD | |
Обзор USB-CAN переходника USB2CANFD_V1 | |
STM32: контроллер bxCAN | https://microsin.net/programming/arm/stm32-controller-bxcan.html?ysclid=mk9qdvxand226707320 |
CAN-шина (Теория) | |
Зачем Программисту Микроконтроллеров Диофантовы Уравнения | |
Using CAN (bxCAN) in Normal mode with STM32 microcontrollers (Part 1) | |
Использование модулей CAN на STM32 для разных целей | |
STM32 CAN Basics | https://www.compilenrun.com/docs/iot/stm32/stm32-communication-protocols/stm32-can-basics/ |
STM32 CAN | |
STM32, CMSIS, CAN, Часть 1 — передача | https://habr.com/ru/articles/598505/?ysclid=mk9qcqafv4623850222 |
STM32 и протокол CAN. Настройка в STM32CubeMx. | https://microtechnics.ru/stm32-i-protokol-can-nastrojka-v-stm32cubemx/ |
CAN-шина и stm32 - часть вторая - фильтры и два CAN |
Вопросы
--Как в bxCAN на STM32 при помощи ID + Mask задать аппаратный запрет на прием только одного конкретного ID (Например 0x55) и одновременно разрешение на пропускание всех остальных ID (...0x53 0x54 0x56 0x57 ...)? Никак
--Можно ли на STM32 так настроить CAN, чтобы он только слушал CAN шину при этом даже не ставя ACK на передаваемых пакетах? При этом чтобы работали CAN фильтры, чтобы генерировались прерывания на прием и всё прочее. Да, можно. Для этого есть отдельный режим: bxCAN in silent mode
--Должен ли CAN выставлять ACK бит если ID не его? Да
--Как измерить процент загрузки CAN шины трафиком в реальном времени? Какой для этого нужен измерительный прибор?
--Допустим мы как-то решили разбить один CAN бит на N квантов. Пусть N=24. Как теперь распределить кванты на интервалы CAN бита: sync, prop, seg1 и seg2?
--Что делать если надо отправить CAN пакет, а все почтовые ящики на отправку заняты и ждут освобождения CAN шины?
--Как определить сколько time quantum (Tq) следует выделить предделителем для одного CAN бита? 10? 20? 40? Проще говоря, какое разрешение нужно для одного CAN бита? Это можно как-то математически обосновать и рассчитать? На сколько квантов разбить один CAN бит?
