
Всем привет!
В данной статье я хочу Вам рассказать про датчик HEDR(от компании avago technologies) - это двухканальный инкрементальный оптический датчик, предназначен для измерения пройденного пути, линейной скорости, угловой скорости и направлении вращения вала.
С помощью данного датчика будет реализован энкодер на базе микроконтроллера STM32, который будет производить вычисление пройденного пути.
В данной статье будет рассмотрено:
Принцип работы датчика HEDR-5420-ES214;
Схема подключения к микроконтроллеру STM32;
Программная реализация (расчет пройденного пути и вывод информации на дисплей).
Технические характеристики датчика HEDR-5420-ES214
Напряжение питания [ 4.5 - 5.5В ];
Тип выхода [ квадратурный ];
Диаметр вала [ 5 мм ];
Разрешение [ 200 отсчетов на оборот ];
Рабочая температура [ от -10°C до +85°C ].
Принцип работы датчика HEDR-5420-ES214
Устройство состоит из трех основных компонентов:
Источник света (светодиод, формирующий поток света);
Оптическая система (линза, обеспечивает фокусировку и отражение света);
Фотодетектор.

Линза фокусирует излучаемый свет на кодовое колесо (диск с чередующимися отражающими и неотражающими участками), при вращении диска, отраженный свет проходит обратно через оптическую систему и попадает на фотодиоды, таким образом на их поверхности формируется чередующийся рисунок света и тени, соответствующий узору кодового диска.
Эти изменения интенсивности света преобразуются в внутренние сигналы А и В, которые проходят через компараторы в составе обработки сигналов, на выходе формируются два цифровых прямоугольных сигнала - канал А и В, находящиеся в квадратурной фазе на 90°, что позволяет микроконтроллеру определять направление вращения вала, к примеру:
если канал А опережает канал B - вращение происходит в одну сторону;
если канал B опережает канал А - вращение происходит в противоположную сторону.
Для своей задачи применяется следующая последовательность, если канал А опережает канал B - движение энкодера считается положительным, если на оборот, то движение будет отрицательным.

Схема подключения к микроконтроллеру STM32


В данной схеме используются преобразователь напряжения DA1 (+12V +5V) и стабилизатор напряжения DA2, дисплей подключается к выводам МК 21_SCL_I2C2 и 22_SDA_I2C2, датчик HEDR подключается к выводам МК 29_CH.A и 30_CH.B, данные сигналы сначала проходят через делители, R17-R18-[CH.A] и R15-R16-[CH.B], так как датчик работает от +5V, сигналы соответственно тоже у него +5V, я всегда стараюсь дополнительно защитить МК, после делителя амплитуда сигналов снизится до +3.3V, копипастить информацию по описанию узлов преобразователя, стабилизатора, узла обвязки напряжения питания и резонатора для МК не особо хочется, поэтому кому интересно можно почитать статью [https://habr.com/ru/articles/950818/].
Прикладываю модуль main.c (конфигурация микроконтроллера)
main.c
/* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** * @attention * * Copyright (c) 2025 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** */ /* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "./Project/proj_main.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ I2C_HandleTypeDef hi2c2; TIM_HandleTypeDef htim1; /* USER CODE BEGIN PV */ /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_TIM1_Init(void); static void MX_I2C2_Init(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_TIM1_Init(); MX_I2C2_Init(); /* USER CODE BEGIN 2 */ /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { proj_main(); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL6; RCC_OscInitStruct.PLL.PREDIV = RCC_PREDIV_DIV1; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1) != HAL_OK) { Error_Handler(); } } /** * @brief I2C2 Initialization Function * @param None * @retval None */ static void MX_I2C2_Init(void) { /* USER CODE BEGIN I2C2_Init 0 */ /* USER CODE END I2C2_Init 0 */ /* USER CODE BEGIN I2C2_Init 1 */ /* USER CODE END I2C2_Init 1 */ hi2c2.Instance = I2C2; hi2c2.Init.Timing = 0x2010091A; hi2c2.Init.OwnAddress1 = 0; hi2c2.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c2.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c2.Init.OwnAddress2 = 0; hi2c2.Init.OwnAddress2Masks = I2C_OA2_NOMASK; hi2c2.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c2.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c2) != HAL_OK) { Error_Handler(); } /** Configure Analogue filter */ if (HAL_I2CEx_ConfigAnalogFilter(&hi2c2, I2C_ANALOGFILTER_ENABLE) != HAL_OK) { Error_Handler(); } /** Configure Digital filter */ if (HAL_I2CEx_ConfigDigitalFilter(&hi2c2, 0) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN I2C2_Init 2 */ /* USER CODE END I2C2_Init 2 */ } /** * @brief TIM1 Initialization Function * @param None * @retval None */ static void MX_TIM1_Init(void) { /* USER CODE BEGIN TIM1_Init 0 */ /* USER CODE END TIM1_Init 0 */ TIM_Encoder_InitTypeDef sConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; /* USER CODE BEGIN TIM1_Init 1 */ /* USER CODE END TIM1_Init 1 */ htim1.Instance = TIM1; htim1.Init.Prescaler = 0; htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 65535; htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter = 0; htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; sConfig.EncoderMode = TIM_ENCODERMODE_TI12; sConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler = TIM_ICPSC_DIV1; sConfig.IC1Filter = 3; sConfig.IC2Polarity = TIM_ICPOLARITY_RISING; sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler = TIM_ICPSC_DIV1; sConfig.IC2Filter = 3; if (HAL_TIM_Encoder_Init(&htim1, &sConfig) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN TIM1_Init 2 */ /* USER CODE END TIM1_Init 2 */ } /** * @brief GPIO Initialization Function * @param None * @retval None */ static void MX_GPIO_Init(void) { /* USER CODE BEGIN MX_GPIO_Init_1 */ /* USER CODE END MX_GPIO_Init_1 */ /* GPIO Ports Clock Enable */ __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /* USER CODE BEGIN MX_GPIO_Init_2 */ /* USER CODE END MX_GPIO_Init_2 */ } /* USER CODE BEGIN 4 */ /* USER CODE END 4 */ /** * @brief This function is executed in case of error occurrence. * @retval None */ void Error_Handler(void) { /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ __disable_irq(); while (1) { } /* USER CODE END Error_Handler_Debug */ } #ifdef USE_FULL_ASSERT /** * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None */ void assert_failed(uint8_t *file, uint32_t line) { /* USER CODE BEGIN 6 */ /* User can add his own implementation to report the file name and line number, ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ /* USER CODE END 6 */ } #endif /* USE_FULL_ASSERT */
Настройка микроконтроллера STM32F030CCTx в CubeIDE

Настройка RCC и SYS (в RCC выбираю Crystal/Ceramic Resonator, так как у меня внешний кварц на 8 МГц)

Настройка дисплея
Взаимодействие дисплея с МК будет через I2C2

Настройка выводов узла подключения датчика HEDR
TIM1_CH1 (к данному выводу будет подключаться сигнал CH.A);
TIM1_CH2 (к данному выводу будет подключаться сигнал CH.B).


Таймер используется в режиме Encoder mode - это специальный аппаратный режим, который позволяет микроконтроллеру автоматически подсчитывать импульсы от инкрементального датчика и определять направление вращения, данная конфигурация освобождает МК от необходимости программно обрабатывать прерывания по каждому импульсу.
Encoder Mode TI1 and TI2 данный параметр указывает, что используется оба канала датчика (A и B), это дает разрешение X4 - т.е. счетчик будет увеличиваться на 4 шага за один полный оборот.
Описание режимов
TI1 - подсчет ведется по фронту одного канала А, направление определяется по уровню В, разрешение 1.8 градусов;
TI2 - аналогично логике TI1, но базируется на канале В;
TI1 and TI2 - подсчет ведется на каждом фронте обоих каналов (А+, А-, В+, В-), направление определяется автоматически, т.е. количеством импульсов на оборот 200, я получаю 800 шагов на оборот, разрешение будет 0.45 градусов.
Input Filter - включает цифровую фильтрацию входного сигнала, помогает убрать дребезг и шум, значения от 0 до 15, чем выше значение, тем надежнее фильтрация, но будет повышаться задержка.
Polarity (Rising Edge) - счетчик реагирует на восходящие фронты сигнала.
Настройка Clock

Программная реализация ведомого устройства
Ссылка на скачивание исходного кода [ https://t.me/ChipCraft В закрепленном сообщении [ #исскуствомк_исходный_код — Исходный код для Encoder_HEDR_5420_STM32F030CCTx].
Модуль process_Encoder
Данный модуль реализует считывание сигналов с инкрементального датчика HEDR и вычисляет:
Количество импульсов на оборот;
Пройденную дистанцию;
Отображение данных на дисплее.
#define ENCODER_MODE_X4 4
Данный параметр отражает режим подсчета импульсов, привожу формулу
Этот режим обеспечивает максимальную точность - 0.45 на один шаг.
#define WHEEL_DIAMETR_M 0.230f // 230 мм #define WHEEL_RADIUS_M (WHEEL_DIAMETR_M / 2.0f)
Здесь я задаю геометрические размеры колеса, на валу которого установлен датчик%
Диаметр колеса 230 мм (0.230м);
Радиус вычисляется так:
#define STEPS_PER_REV (ENCODER_PPR * ENCODER_MODE_X4)
Максимальное количество шагов за один оборот
#define CIRCUMFERENCE_M (2.0f *M_PI * WHEEL_RADIUS_M)
Длина окружности колеса - это путь, который проходит колесо за один оборот
т.е. при моем радиусе 0.115, получится за один полный оборот 0.72 м.

Функция display_init() - инициализация дисплея
Инициализируется драйвер дисплея;
Выполняется заливка экрана черным цветом;
На дисплее на 2 секунды отображается стартовый экран с надписью ''ChipCraft";
После задержки экран очищается для дальнейшей работы.
библиотеку для работы с дисплеем я взял с [https://github.com/afiskon/stm32-ssd1306/tree/master]
Функция display_update()
Отвечает за визуализацию информации на дисплее:
Экран предварительно очищается с помощью ssd1306_Fill(Black);
В верхней части по центру отображается надпись «Encoder»;
Ниже последовательно выводятся:
количество импульсов;
дистанция;
Буфер графики передается на дисплей вызовом ssd1306_UpdateScreen()
Функция encoder_Handler()
Логика работы:
считывание текущего значения таймера;
определение разницы (delta) между текущим и предыдущим значениями;
накопление общего счетчика enocoder_position;
вызов функций для вычисления дистанции и обновление дисплея.
Функция get_distance_m() - вычисление пройденной дистанции
Переводит количество импульсов датчика в физическую длину пути в (метрах).
process_Encoder.c
#include "./Project/process_Encoder.h" #include "./Project/shared.h" #include "./Project/ssd1306.h" #include "./Project/ssd1306_fonts.h" #include "main.h" #include <stdlib.h>//abs #include <string.h>//memset #include <stdio.h> #include <stdint.h> #include <math.h> #define ENCODER_PPR 200 // импульсов на оборот #define ENCODER_MODE_X4 4 #define WHEEL_DIAMETR_M 0.230f // 230 мм #define WHEEL_RADIUS_M (WHEEL_DIAMETR_M / 2.0f) #define STEPS_PER_REV (ENCODER_PPR * ENCODER_MODE_X4) #define CIRCUMFERENCE_M (2.0f *M_PI * WHEEL_RADIUS_M) uint16_t current_count= 0; int16_t delta = 0; float distance = 0.0f; int32_t encoder_position = 0; uint8_t ssd1306_buffer[SSD1306_BUFFER_SIZE]; void display_init(void) { ssd1306_Init(); ssd1306_Fill(Black); ssd1306_SetCursor(20, 25); ssd1306_WriteString("ChipCraft", Font_11x18, White); ssd1306_UpdateScreen(); HAL_Delay(2000); ssd1306_Fill(Black); ssd1306_UpdateScreen(); } void encoder_Handler(void) { static uint16_t last_count = 0; current_count = __HAL_TIM_GET_COUNTER(&htim1); delta = (int16_t)(current_count - last_count); encoder_position += delta; last_count = current_count; get_distance_m(); display_update(encoder_position, distance); } void display_update(int32_t pulses, float distance) { char buf[32]; ssd1306_Fill(Black); ssd1306_SetCursor(25, 2); ssd1306_WriteString("Encoder" ,Font_11x18, White); sprintf(buf, "Pulses: %ld", pulses); ssd1306_SetCursor(2, 22); ssd1306_WriteString(buf, Font_7x10, White); sprintf(buf, "Dist: %.2f m", distance); ssd1306_SetCursor(2, 36); ssd1306_WriteString(buf, Font_7x10, White); ssd1306_UpdateScreen(); } float get_distance_m(void){ distance = ((float) encoder_position / STEPS_PER_REV) * CIRCUMFERENCE_M; return distance; }
Модуль proj_main() - главный метод
Выполняется инициализация дисплея;
Запуск таймера;
Запуск функции encoder_Handler().
proj_main.c
#include "./Project/shared.h" #include "./Project/proj_main.h" #include "./Project/process_Encoder.h" #include "./Project/process_Encoder.h" void proj_main() { volatile const char *ch = ";V-F-BIN;ver: "VER_PROG(VER_a,VER_b,VER_c);(void)ch;//0x8008b00 display_init(); HAL_TIM_Encoder_Start(&htim1, TIM_CHANNEL_ALL); while (1){ //хэндлеры encoder_Handler(); }//while (1) }
Если статья показалась Вам интересной, буду рад выпустить для Вас еще множество статей исследований по всевозможным видам устройств, так что, если не хотите их пропустить — буду благодарен за подписку на мой ТГ-канал.
