CAN — Controller Area Network
CAN шина это промышленный последовательный протокол связи, разработанный компанией Bosch в 1980-х годах для автомобильной электроники. Сегодня её используют не только в автомобилях, но и в промышленной автоматике, медицинской технике, робототехнике и других областях.
По CAN шине передача данных осуществляется с высокой надежностью, т.к. есть встроенный контроль ошибок а провода по которым передаются данные являются витой парой, а значит если будет воздействие магнитного поля то конечный сигнал всё равно останется прежним. CAN шина является многоточечной и в отличии от других интерфейсов на ней нет master и slave устройств, вместо этого в CAN-шине действует принцип многоведения (multi-master). Любое устройство (узел) может начать передачу в любой момент. Для разрешения конфликтов используется арбитраж по идентификатору сообщения, о нём мы поговорим чуть позже.
Физический уровень CAN шины.
Давайте разберемся что конкретно нам надо чтобы подключиться к CAN шине. Допустим у вас есть несколько устройств и вы хотите соединить их по CAN шине. Для этого нам понадобятся:
- Само устройство, это может быть микроконтроллер, датчик, Arduino и т.п Это то что обрабатывает данные и решает отправить или читать
- CAN контроллер. Он формирует и разбирает кадры, управляет арбитражем, ошибками.
- Трансивер. Устройство (микросхема), она преобразует логические уровни CAN-контроллера (TX/RX) в дифференциальный сигнал на линии CANH / CANL
- Витая пара
- Два терминатора (резисторы по 120 ОМ)
Вот собственно и весь набор. Резисторов нужно всего два на всю линию и они обязательно должны быть 120 ОМ, остальное железо (микроконтроллер, CAN контроллер, трансивер) необходимо своё для каждого узла.
Схема соединения следующая.
Все устройства (ECU, датчики, контроллеры) подключаются к двум основным линиям: CAN_H (High) и CAN_L (Low). В каждом узле есть трансивер, который подключается к этим двум линиям. Тоесть фактически мы получаем параллельное соединение.

Теперь пару слов о CAN контроллере и трансивере.
CAN контроллер — это мозг, который отвечает за работу с протоколом CAN. Его основная задача это управление обменом данными так, чтобы оставить только полезные данные для вашего устройства, а всю остальную работу забирает на себя. Он занимается формированием и приёмом кадров, проверяет целостность. Если возникают конфликты, участвует в арбитраже. Если же возникают ошибки, занимается повторной отправкой. Бывают двух видов. Внутренний (или встроенный) и внешний. Например у STM32F407 он интегрирован внутрь. Если же например использовать Arduino UNo то там внутри нет встроенного и придется использовать внешний, например MCP2515.
Трансивер — это микросхема, которая преобразует цифровые сигналы CAN контроллера в дифференциальные сигналы которые передаются по витой паре. То есть занимается преобразованием уровней CAN-контроллера (TXD / RXD) на линию CAN (CAN_H / CAN_L).
Что значит преобразование в дифференциальный сигнал? Допустим CAN контроллер выдаёт 1. Для трансивера это рецессивный бит, и трансивер выдаёт CAN_H 2,5В и CAN_L 2,5В. Сигнал дифференциальный, то есть считается разница между двумя линиями, 2.5-2.5=0 В
Если же CAN контроллер выдаёт 0. Это доминантный сигнал и наш трансивер выдаёт CAN_H 3,5В и CAN_L 1,5В. 3.5-1.5=2 В
Трансивер так же занимается защитой от КЗ и статического разряда.
CAN шина обладает достаточно высокой скоростью, которая зависит от расстояние на которое необходимо передать сигнал. Типичные значения для стандартной CAN-шины (CAN 2.0)
- 1 Мбит/с: Максимальная длина шины составляет примерно 40 метров.
- 500 кбит/с: Максимальная длина увеличивается до 100 метров.
- 125 кбит/с: Длина может достигать 500 метров.
- 10 кбит/с: При такой низкой скорости дальность может достигать 1000 метров
Разъёмы.
Для подключения к CAN-шине в автомобилях наиболее распространённым решением является разъем OBD-II (он же диагностический DLC), который стал стандартом и используется практически во всех современных машинах для диагностики и обмена данными с электронными блоками управления. В промышленной автоматике и оборудовании гораздо чаще применяют разъем D-Sub 9 (DB9), закрепленный в спецификациях CANopen и DeviceNet. Однако стоит понимать, что выбор разъёмов в разных сферах применения CAN-шины не ограничивается только этими вариантами. Существует множество других типов коннекторов: от герметичных промышленных M12 и удобных RJ45, до простых клеммников и штырьковых разъемов, используемых в прототипах и разработке. Таким образом, стандарт CAN определяет лишь электрические сигналы и топологию сети, а конкретный тип разъема зависит от области применения и требований к надёжности, удобству или защите соединений.
Разъём OBD

Разъём D-SUB 9

Формат кадра.
В CAN-шине обмен данными построен на кадрах (frames). Каждый кадр имеет строго определённую структуру и назначение. Всего в стандарте CAN различают 4 вида кадров.
Data Frame (кадр данных).
Это основной тип кадра, с помощью которого узлы передают полезную информацию.
Состоит из:
- Start of Frame — SOF: Один бит, который сигнализирует о начале нового кадра.
- Arbitration field – идентификатор (11 или 29 бит, в зависимости от формата) + бит RTR (Remote Transmission Request).
- Control field – указывает длину данных.
- Data field – до 8 байт данных (в классическом CAN). В CAN FD – до 64 байт.
- CRC field – контрольная сумма.
- ACK field – подтверждение другими узлами.
- EOF – 7 битов, которые сигнализируют о конце кадра.
Remote Frame(кадр удаленного запроса).
Используется для запроса данных из другого узла. Как это работает. Узел, которому нужны данные (например, показания температуры), отправляет на шину кадр удаленного запроса с идентификатором, соответствующим нужным данным. Узел-получатель, который имеет нужные данные и настроен на этот идентификатор, распознает запрос, считывает его и в ответ автоматически отправляет кадр данных со своим полем данных, содержащим запрошенную информацию (например, текущее значение температуры).По своей структуре он почти идентичен кадру данных, но имеет одно ключевое отличие: в нем нет поля данных (Data Field), а бит RTR (Remote Transmission Request) устанавливается в доминантное состояние (логический 0).
Error Frame(кадр ошибки)
Если один из узлов фиксирует нарушение формата кадра, он формирует и отправляет в шину Error Frame, который обладает наивысшим приоритетом. Получив этот кадр, остальные узлы понимают, что в сети возникла ошибка, и последний переданный кадр необходимо считать недействительным.
Overload Frame(Кадр перегрузки)
Cлужит для того, чтобы временно задержать передачу следующего кадра.
Его используют узлы, которые по какой-то причине не успевают обработать данные. На практике используется редко.
Кадр ошибки (Error Frame), и Кадр перегрузки (Overload Frame) формируются аппаратно CAN-контроллером узла.
Адресация.
Ключевым принципом адресации CAN шины является то, что устройства в сети не имеют адреса, а вместо этого используется уникальный ID(идентификатор) в каждом передаваемом сообщении. Внутри ID лежит смысловая нагрузка, например ID 0x123 может означать «скорость колеса», а ID 0x456 — «температура двигателя». Все сообщения, передаваемые по CAN-шине, являются широковещательными. Это означает, что каждый узел в сети «слышит» все передаваемые кадры. Если в сеть попадает кадр с нужным ID то его принять и обработать могут сразу несколько устройств, т.к. у них прописана обработка кадра именно с этим ID.
Существует два вида ID. Стандартный — 11 бит(2048 устройств) и расширенный 29 бит( более 500 миллионов устройств).
Арбитраж.
Арбитраж в CAN-шине — это процесс разрешения конфликтов, когда два или более узла пытаются одновременно начать передачу данных. Он основан на принципе доминирования битов и обеспечивает, что только сообщение с наивысшим приоритетом будет передано успешно.
Вся система построена на идентификаторе. Допустим два узла начинают передачу одновременно. Тогда бит за битом происходит сравнение идентификаторов. Как только у одного из сообщений передается доминантный бит (логический 0) а у другого рецессивный(логическая 1), то второе устройство прекращает передачу своего сообщения и слушает первое устройство.Например 0001 побеждает 0010 т.к. доминантный бит идёт раньше рецессивного. Всё это происходит без потери данных т.к. проигравшие узлы не теряют данные.За это всё отвечает CAN контроллер каждого узла.
Реализация CAN шины на практике.
Перейдём к практической реализации. Есть плата Discovery с микроконтроллером STM32F407VGT6. И есть плата ардуино UNO. Наша задача передать данные от ардуино UNO и принять их на STM32 по CAN шине.
Для этого нам понадобятся:
STM32F4 Discovery

Arduino UNO

WCMCU-230

MCP2515

Два резистора 120 ОМ.
Провода витая пара, но в нашем случае можем использовать любые т.к. расстояние очень короткое.
Микроконтроллер STM32F4 уже содержит внутри CAN контроллер но для работы с CAN шиной нужен трансивер. WCMCU-230 — это модуль приемопередатчика (трансивера) CAN-шины, построенный на микросхеме SN65HVD230. Он выполняет роль физического интерфейса который преобразует логику CAN контроллера и реальной CAN шиной, преобразуя сигналы контроллера в дифференциальные сигналы CAN шины.
Со стороны ардуино всё чуть сложнее. Здесь нет встроенного CAN контроллера, поэтому нам понадобится плата MCP2515. Она представляет из себя автономный контроллер CAN-шины, разработанный компанией Microchip. Она берёт на себя всю работу связанную с логикой CAN шины (формат кадров, ACK, CRC, арбитраж и т.д.). На плате также есть трансивер TJA1050 для преобразования логических уровней в дифференциальные сигналы CAN шины. Плата MCP2515 соединяется с ардуино UNO по интерфейсу SPI.
Схема соединения.
Соединяем STM32 с WCMCU-230.
Выбираем вкладку Connectivity-CAN1. Подсвечиваются пины PA11 PA12. И здесь нас поджидает проблема, их нет на нашей плате, они не выведены на внешние пины. Поэтому нужно вручную выбрать другие пины, то есть сделать ремаппинг. Выбираем PB8(CAN_TX) и PB9(CAN_RX). CubeIDE позволяет это сделать вручную.
Соединяем нашу плату STM32F4 Discovery с WCMCU-230.
PB8(CAN RX) — CRX
PB9(CAN TX) — CTX
Соединяем Arduino UNO с MCP2515
D11 — SI
D12 — SO
D13 — SCK
D2 — INT
D10 — CS
Здесь есть один очень важный момент. Плате MCP2515 для питания необходимо стабильные 5 В. На выходе ардуино может выдавать такое напряжение но работать не будет. Поэтому всегда подключайте вашу плату от другого источника питания.
Вторым важным моментом является общий ноль.CAN шина работает на дифференциальном сигнале и если не будет общего нуля то скорее всего получится ошибка передачи данных.
Затем подключаем нашу MCP2515 к WCMCU-230 двумя проводами CAN H и CAN L соответственно.
Настройки CubeIDE.
Рассчитаем необходимые настройки скорости. Необходимо получить скорость в 20 кбит/c Для этого зайдем в настройки Clock Configuration Cube IDE. CAN1 модуль подключен к APB1 peripheral clocks. Нам нужно получить на нём частоту 32МГц. Для этого поставим внешний высокочастотный источник тактирования HSE. Далее в PLL нам необходимо получить 144 МГц. Входная частота 25МГц. Для этого выставим следующие параметры.
PLLM = 25
PLLN = 288
PLLP = 2
Далее AHB prescaler = 1 и наконец APB1 prescaler = 4. Что даст нам 36МГц га APB1.

Далее включаем CAN в CubeIDE. Выставляем следующие настройки.

Первое что нам необходимо настроить это Time Quantum.Time Quantum (tq) — это наименьшая единица времени, используемая для измерения длительности различных сегментов бита в протоколе CAN. Представьте себе бит как отрезок, разделенный на очень маленькие, одинаковые по длине «шаги» или «кванты». Эти «шаги» и есть Time Quantum.Каждый бит в CAN-шине делится на несколько сегментов, каждый из которых состоит из определённого количества Time Quanta.
Высчитывается он достаточно просто. Частоту шины делим на предделитель. В нашем случае это 32000000/100=320000 Затем получаем TQ=1/320000=3.125 мкс
Бит делится на три части.
- SyncSeg (1 Tq) — всегда 1 квант. Отвечает за синхронизацию по ведущему фронту.
- Time Quanta in Bit Segment 1 (TS1) — число Tq в первом сегменте (включает Propagation Segment(компенсация времени сигнала) + Phase Segment 1
- Time Quanta in Bit Segment 1 (TS2) — число Tq во втором сегменте (Phase Segment 2)
Выставим TQ1 = 13 и TQ2=2. Эти числа получаем из формулы .
N=1+TQ1+TQ2
Получаем 16, т.к. при частоте 32 МГц и делителе 100 это даст ровно 20 кбит/с.
При выборе этих значений необходимо также учитывать точку выборки(Sample Point).
Она считается по формуле SP=(1+13)/16=87.5%
87.5 %, что значит, что сигнал считывается ближе к концу битового интервала, что обычно хорошо для стабильной работы на низких скоростях.
Разберем следующий блок настроек.
Time Triggered Communication Mode — режим CAN контроллера для добавления синхронизации по времени.
Automatic Bus-Off Management — настройка для возвращения в работу узла при превышении количества ошибок. Каждый узел имеет счетчик ошибок, и если он превышен то узел выпадает из шины. Эта настройка позволяет автоматически вернуться в работу через определённое количество времени.
Automatic Wake-Up Mode -режим, при котором CAN-контроллер STM32 автоматически выходит из сна при активности на шине. Вся эта система служит для экономии энергии.
Automatic Retransmission — если включено, то заставляет контроллер передавать кадры заново при неудачной отправке.
Receive Fifo Locked Mode — настройка завязанная на буферы RX_FIFO_0 и RX_FIFO_1. Если отключено то новые значения могут переписать значения в этих буферах.
Transmit Fifo Priority — CAN-контроллер имеет трансмиссионный буфер (Transmit FIFO) для исходящих сообщений. Обычно 3–4 ячейки (Mailbox 0, 1, 2). CPU помещает кадр в свободный Mailbox, контроллер сам отправляет их на шину. Если настройка выключена то CAN шина отправляет кадры по приоритету (id) и может случиться так что кадр с низким приоритетом зависнет в Mailbox т.к. всё время будут приходить кадры с более высоким приоритетом.
Operating Mode — Normal (обычная работа шины), Loopback (тестирование без включения к реальной шине), Silent (мониторинг CAN-шины без влияния на неё), Loopback combined with Silent (полное тестирование программного CAN-протокола)
В итоге получаем следующие настройки.

Код в CubeIDE.
Разберем код в CubeIDE для нашего микроконтроллера.
Сначала объявим нужные переменные
uint32_t counter = 0;
CAN_TxHeaderTypeDef TxHeader;
CAN_RxHeaderTypeDef RxHeader;
uint8_t TxData[8] = {0};
uint8_t RxData[8];
uint32_t TxMailbox;
/* USER CODE END PV */
Добавим функцию мигания светодиода. На плате discovery есть встроенные светодиоды, будем использовать их для индикации работы can шины.
/* USER CODE BEGIN 0/
void BlinkError(uint8_t count)
{
for (uint8_t i = 0; i < count; i++)
{
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_13, GPIO_PIN_SET);
HAL_Delay(150);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_13, GPIO_PIN_RESET);
HAL_Delay(150);
}
HAL_Delay(500);
}
/USER CODE END 0 */
Далее, т.к. у нас всего одно принимающее устройство настроим его на приём всех входящих сообщений независимо от идентификатора.
CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank = 0; sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh = 0x0000; sFilterConfig.FilterIdLow = 0x0000; sFilterConfig.FilterMaskIdHigh = 0x0000; sFilterConfig.FilterMaskIdLow = 0x0000; sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; sFilterConfig.FilterActivation = ENABLE; sFilterConfig.SlaveStartFilterBank = 14;
Применим настройки фильтра к контроллеру CAN1 и проверим успешно ли прошла конфигурация. Если нет моргнём три раза красным светодиодом.
if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
{
BlinkError(3);
Error_Handler();
}
Этот код запускает работу CAN-контроллера в нормальном режиме с помощью HAL_CAN_Start(). Если инициализация или запуск не удались, программа мигает светодиодом пять раз для индикации ошибки и переходит в обработчик ошибок (Error_Handler()), останавливая выполнение.
// Запускаем CAN
if (HAL_CAN_Start(&hcan1) != HAL_OK)
{
BlinkError(5);
Error_Handler();
}
Этот код включает прерывание CAN_IT_RX_FIFO0_MSG_PENDING, которое срабатывает, когда в буфере приёма FIFO0 появляется новое сообщение. Если активация прерываний не удалась, контроллер мигает светодиодом семь раз и останавливает выполнение через Error_Handler().
// Включаем прерывания при приёме
if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
BlinkError(7);
Error_Handler();
}
Напишем функцию которая будет автоматически вызываться при срабатывании прерывания, когда в FIFO0 поступает новое CAN сообщение. Она считывает принятый кадр в структуры RxHeader и RxData, а при успешном приёме мигает светодиодом PD12 для индикации активности шины; если чтение не удалось, вызывает мигание с кодом ошибки 2.
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK)
{
// мигнуть светодиодом PD12 при каждом принятом кадре
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}
else
{
BlinkError(2); // ошибка при чтении
}
}
Код для arduino uno.
Рассмотрим код необходимый для передачи сообщения по CAN шине для ардуино уно.
Первым делом подключим нужные библиотеки
#include mcp_can.h; #include SPI.h;
Зададим пины ардуино к которым подключён CAN-контроллер MCP2515: пин 10 используется как Chip Select (CS) для связи по SPI, а пин 2 — как вход прерывания (INT), который контроллер может использовать для уведомления Arduino о получении новых сообщений.
#define CAN_CS 10 // Пин CS для MCP2515 #define CAN_INT 2 // Пин INT (можно не использовать в режиме передачи)
Создадим объект класса MCP_CAN
MCP_CAN CAN(CAN_CS);
Напишем функцию инициализации SPI повторяя попытку при неудаче, и переводя контроллер в нормальный режим работы для отправки CAN сообщений.
void setup() {
Serial.begin(115200);
Serial.println(F("Initializing MCP2515 CAN module..."));
// Инициализация CAN-контроллера (скорость 5 кбит/с, кварц 8 МГц)
while (CAN.begin(MCP_ANY, CAN_20KBPS, MCP_8MHZ) != CAN_OK) {
Serial.println(F("CAN init FAIL, retrying..."));
delay(1000);
}
Serial.println(F("CAN init OK."));
CAN.setMode(MCP_NORMAL); // Переход в нормальный режим работы (не loopback)
delay(1000);
}
В функции loop() формируется массив из 8 байт данных и отправляется стандартный CAN-кадр с ID 0x123 через MCP2515; после каждой отправки выводится сообщение об успехе или ошибке, и цикл повторяется каждую секунду.
void loop() {
// Пример данных для отправки (8 байт)
byte data[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
// Отправка стандартного кадра с ID = 0x123
byte sndStat = CAN.sendMsgBuf(0x123, 0, 8, data);
if (sndStat == CAN_OK) {
Serial.println(F("Message Sent Successfully!"));
} else {
Serial.println(F("Error Sending Message..."));
}
delay(1000); // Отправляем раз в секунду
}
Проверка на практике.
Для проверки на практике зайдём в CubeIDE и включим режим отладки.
![]()
Добавим RxData в Expressions и установим точку останова в конце функции HAL_CAN_RxFifo0MsgPendingCallback.
Запустим отладку. У нас должен загореться зеленый светодиод на плате Discovery и в Expressions видим следующее.

Напомню что передавали мы byte data[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; Если внимательно посмотреть то будет видно что
\021 = 0x11
\» = 0x22
3 = 0x33
D = 0x44
U = 0x55
f = 0x66
w = 0x77
\210 = 0x88
То есть нам пришло именно то что мы отправили а значит наша can шина работает как положено.