Всем привет!

В данной статье я хочу Вам рассказать про датчик 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 - движение энкодера считается положительным, если на оборот, то движение будет отрицательным.

Осциллограмма данных полученных с датчика HEDR-5420-ES214
Осциллограмма данных полученных с датчика HEDR-5420-ES214

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

Схема подключения HEDR и дисплея к микроконтроллеру STM32F030CCT6
Схема подключения HEDR и дисплея к микроконтроллеру STM32F030CCT6
Макет STM32F030CCTx и HEDR
Макет STM32F030CCTx и HEDR

В данной схеме используются преобразователь напряжения 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

Данный параметр отражает режим подсчета импульсов, привожу формулу

N_{шагов} = PPR  \cdot 4 = 200 \cdot 4 = 800 O_{шаг} = \frac{360}{800} = 0.45

Этот режим обеспечивает максимальную точность - 0.45 на один шаг.

#define WHEEL_DIAMETR_M 0.230f // 230 мм
#define WHEEL_RADIUS_M (WHEEL_DIAMETR_M / 2.0f)

Здесь я задаю геометрические размеры колеса, на валу которого установлен датчик%

Диаметр колеса 230 мм (0.230м);

Радиус вычисляется так:

R = \frac{ D}{2} = \frac{ 0.230}{2} = 0.115 м
#define STEPS_PER_REV (ENCODER_PPR * ENCODER_MODE_X4)

Максимальное количество шагов за один оборот

#define CIRCUMFERENCE_M (2.0f *M_PI * WHEEL_RADIUS_M)

Длина окружности колеса - это путь, который проходит колесо за один оборот

C = 2\pi RC = 2 \cdot 3.1416 \cdot 0.115 = 0.722 м

т.е. при моем радиусе 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)


  1. diakin
    01.11.2025 05:35

    Не очень понятно в даташите, какой там выход? Открытый коллектор? Такое впечатление, что ТТЛ, судя по токам.
    А на осциллограмме выше - это сигнал с выхода датчика или после делителя?

    Некоторые характеристики TTL-выхода:

    • Напряжение логического нуля: не выше 0,8 В при рабочем выходном токе 8 мА.

    • Напряжение логической единицы: не ниже 2 В при рабочем выходном токе -0,4 мА.

    Не теряются импульсы при высокой скорости вращения? Можно поставить метку на валу, быстро покрутить туда-сюда и снова выставить на метку. Должен вернуться в +-0. Или 360 )


    1. DM_ChipCraft Автор
      01.11.2025 05:35

      Это сигналы после делителя, тестировал конечно же как Вы и написали, ставил метку на оси и быстро крутил, в 0 возвращается, этот датчик я применяю при пешеходной съемке, максимальная скорость у меня была 4-5 км/ч, можно конечно сделать тест , посадить датчик на шаговый двигатель какой нибудь, кстати попробую:)

      Выход у датчика TTL, все верно

      Спасибо за комментарий


  1. mmMike
    01.11.2025 05:35

    Ссылка на скачивание исходного кода [ https://t.me/ChipCraft

    буду благодарен за подписку на мой ТГ-канал.

    На что только люди не идут что бы на них подписались...
    Целая статья в которой специально нет ключевых подробностей.
    Соблазн посмотреть правильный ли режим таймера STM32 (для работы с энкодером) был выставлен, был мной у себя задавлен с мыслью "Да и пофиг, что там автор сделал. Как нужно я и так знаю".

    Достали статьи с лейтмотивом "ну подпишитесь на мой TG канал, что вам стоит то".


    1. DM_ChipCraft Автор
      01.11.2025 05:35

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


      1. mmMike
        01.11.2025 05:35

        ну ну.
        Я вот только по косвенным признакам (Описание режимов..TI1 and TI2) могу догадаться что используется режим работы с энкодером таймера (исходники то инициализации таймера только по ссылке на TG)

        Поэтому ценность статьи сомнительная. Для тех кто в теме - ничего нового.
        Для тех кто не совсем в теме - "раз раз и работает" (без пояснения ключевых мест). Типа скачайте с TG..
        Угу. Скачайте..

        Да еще HAL либа (хотя это дело вкуса).


        1. DM_ChipCraft Автор
          01.11.2025 05:35

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


          1. mmMike
            01.11.2025 05:35

            Извините, если что.
            но что то триггерят статьи у которых "подпишитесь на мой канал".
            А тут еще исходники на TG канале, а не github, например.

            минусы в таких случая не ставлю. Но побурчать... без проблем (пока релиз кандидат собирается и тесты прогоняются.. хоть отвлечься от работы)


            1. DM_ChipCraft Автор
              01.11.2025 05:35

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


  1. randomsimplenumber
    01.11.2025 05:35

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


  1. MikeNer
    01.11.2025 05:35

    Глянул канал в телеге, расхотелось подписываться, в названии 2 орфографических ошибки, не исскуство, а искусство, и не миксросхем, а микросхем, наверное. Исправьте, пожалуйста.