
Всем привет!
В данной статье я хочу Вам рассказать про датчик 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)
}Если статья показалась Вам интересной, буду рад выпустить для Вас еще множество статей исследований по всевозможным видам устройств, так что, если не хотите их пропустить — буду благодарен за подписку на мой ТГ-канал.
Комментарии (10)

mmMike
01.11.2025 05:35Ссылка на скачивание исходного кода [ https://t.me/ChipCraft
буду благодарен за подписку на мой ТГ-канал.
На что только люди не идут что бы на них подписались...
Целая статья в которой специально нет ключевых подробностей.
Соблазн посмотреть правильный ли режим таймера STM32 (для работы с энкодером) был выставлен, был мной у себя задавлен с мыслью "Да и пофиг, что там автор сделал. Как нужно я и так знаю".Достали статьи с лейтмотивом "ну подпишитесь на мой TG канал, что вам стоит то".

DM_ChipCraft Автор
01.11.2025 05:35Дело каждого, я никого не умоляю подписаться, а по вопросу, настройки таймера и реализации самого кода(с пояснениями), я предоставил всю информацию в данной статье:), надо только внимательно посмотреть статью.

mmMike
01.11.2025 05:35ну ну.
Я вот только по косвенным признакам (Описание режимов..TI1 and TI2) могу догадаться что используется режим работы с энкодером таймера (исходники то инициализации таймера только по ссылке на TG)Поэтому ценность статьи сомнительная. Для тех кто в теме - ничего нового.
Для тех кто не совсем в теме - "раз раз и работает" (без пояснения ключевых мест). Типа скачайте с TG..
Угу. Скачайте..Да еще HAL либа (хотя это дело вкуса).

DM_ChipCraft Автор
01.11.2025 05:35хм, честно говоря у меня не было в планах скрывать информацию о настройке таймера, а уж тем более коварного плана по получению подписчиков:) по факту можно зайти - подписаться (скачать исходник) и отписаться :) я на самом деле думал что достаточно показать настройку таймера в граф.интерфейсе, естественно я не против открыть конфиги, обязательно дополню информацию)

mmMike
01.11.2025 05:35Извините, если что.
но что то триггерят статьи у которых "подпишитесь на мой канал".
А тут еще исходники на TG канале, а не github, например.минусы в таких случая не ставлю. Но побурчать... без проблем (пока релиз кандидат собирается и тесты прогоняются.. хоть отвлечься от работы)

DM_ChipCraft Автор
01.11.2025 05:35Ничего страшного, я прекрасно Вас понимаю, скрывать не буду, я с github особо не работаю, и не умею в нем работать, но обязательно задумаюсь, чтобы исходники заливать и туда:)

randomsimplenumber
01.11.2025 05:35Круто, конечно, что в stm32 есть таймер, который сразу умеет в енкодер. Буду знать ;) перфекционист говорит, что, наверное, у того таймера и прерывания есть ;)

MikeNer
01.11.2025 05:35Глянул канал в телеге, расхотелось подписываться, в названии 2 орфографических ошибки, не исскуство, а искусство, и не миксросхем, а микросхем, наверное. Исправьте, пожалуйста.
diakin
Не очень понятно в даташите, какой там выход? Открытый коллектор? Такое впечатление, что ТТЛ, судя по токам.
А на осциллограмме выше - это сигнал с выхода датчика или после делителя?
Некоторые характеристики TTL-выхода:
Напряжение логического нуля: не выше 0,8 В при рабочем выходном токе 8 мА.
Напряжение логической единицы: не ниже 2 В при рабочем выходном токе -0,4 мА.
Не теряются импульсы при высокой скорости вращения? Можно поставить метку на валу, быстро покрутить туда-сюда и снова выставить на метку. Должен вернуться в +-0. Или 360 )
DM_ChipCraft Автор
Это сигналы после делителя, тестировал конечно же как Вы и написали, ставил метку на оси и быстро крутил, в 0 возвращается, этот датчик я применяю при пешеходной съемке, максимальная скорость у меня была 4-5 км/ч, можно конечно сделать тест , посадить датчик на шаговый двигатель какой нибудь, кстати попробую:)
Выход у датчика TTL, все верно
Спасибо за комментарий