- •Оглавление
- •Предисловие
- •Почему я написал книгу?
- •Для кого эта книга?
- •Как использовать эту книгу?
- •Как организована книга?
- •Об авторе
- •Ошибки и предложения
- •Поддержка книги
- •Как помочь автору
- •Отказ от авторского права
- •Благодарность за участие
- •Перевод
- •Благодарности
- •I Введение
- •1. Введение в ассортимент микроконтроллеров STM32
- •1.1. Введение в процессоры на базе ARM
- •1.1.1. Cortex и процессоры на базе Cortex-M
- •1.1.1.10. Внедренные функции Cortex-M в ассортименте STM32
- •1.2. Введение в микроконтроллеры STM32
- •1.2.1. Преимущества ассортимента STM32….
- •1.2.2. ….И его недостатки
- •1.3. Краткий обзор подсемейств STM32
- •1.3.1. Серия F0
- •1.3.2. Серия F1
- •1.3.3. Серия F2
- •1.3.4. Серия F3
- •1.3.5. Серия F4
- •1.3.6. Серия F7
- •1.3.7. Серия H7
- •1.3.8. Серия L0
- •1.3.9. Серия L1
- •1.3.10. Серия L4
- •1.3.11. Серия L4+
- •1.3.12. Серия STM32WB
- •1.3.13. Как правильно выбрать для себя микроконтроллер?
- •1.4. Отладочная плата Nucleo
- •2. Установка инструментария
- •2.1. Почему выбирают Eclipse/GCC в качестве инструментария для STM32
- •2.1.1. Два слова о Eclipse…
- •2.2. Windows – Установка инструментария
- •2.2.1. Windows – Установка Eclipse
- •2.2.2. Windows – Установка плагинов Eclipse
- •2.2.3. Windows – Установка GCC ARM Embedded
- •2.2.4. Windows – Установка инструментов сборки
- •2.2.5. Windows – Установка OpenOCD
- •2.2.6. Windows – Установка инструментов ST и драйверов
- •2.3. Linux – Установка инструментария
- •2.3.2. Linux – Установка Java
- •2.3.3. Linux – Установка Eclipse
- •2.3.4. Linux – Установка плагинов Eclipse
- •2.3.5. Linux – Установка GCC ARM Embedded
- •2.3.6. Linux – Установка драйверов Nucleo
- •2.3.7. Linux – Установка OpenOCD
- •2.3.8. Linux – Установка инструментов ST
- •2.4. Mac – Установка инструментария
- •2.4.1. Mac – Установка Eclipse
- •2.4.2. Mac – Установка плагинов Eclipse
- •2.4.3. Mac – Установка GCC ARM Embedded
- •2.4.4. Mac – Установка драйверов Nucleo
- •2.4.5. Mac – Установка OpenOCD
- •2.4.6. Mac – Установка инструментов ST
- •3. Hello, Nucleo!
- •3.1. Прикоснитесь к Eclipse IDE
- •3.2. Создание проекта
- •3.3. Подключение Nucleo к ПК
- •3.5. Изучение сгенерированного кода
- •4. Инструмент STM32CubeMX
- •4.1. Введение в инструмент CubeMX
- •4.1.1. Представление Pinout
- •4.1.2. Представление Clock Configuration
- •4.1.3. Представление Configuration
- •4.1.4. Представление Power Consumption Calculator
- •4.2. Генерация проекта
- •4.2.1. Генерация проекта Си при помощи CubeMX
- •4.2.2. Создание проекта Eclipse
- •4.2.3. Ручное импортирование сгенерированных файлов в проект Eclipse
- •4.3. Изучение сгенерированного кода приложения
- •4.3.1. Добавим что-нибудь полезное в микропрограмму
- •4.4. Загрузка исходного кода примеров книги
- •5. Введение в отладку
- •5.1. Начало работы с OpenOCD
- •5.1.1. Запуск OpenOCD
- •5.1.2. Подключение к OpenOCD Telnet Console
- •5.1.3. Настройка Eclipse
- •5.1.4. Отладка в Eclipse
- •5.2. Полухостинг ARM
- •5.2.1. Включение полухостинга в новом проекте
- •5.2.2. Включение полуохостинга в существующем проекте
- •5.2.3. Недостатки полухостинга
- •5.2.4. Как работает полухостинг
- •II Погружение в HAL
- •6. Управление GPIO
- •6.2. Конфигурация GPIO
- •6.2.1. Режимы работы GPIO
- •6.2.2. Режим альтернативной функции GPIO
- •6.2.3. Понятие скорости GPIO
- •6.3. Управление GPIO
- •6.4. Деинициализация GPIO
- •7. Обработка прерываний
- •7.1. Контроллер NVIC
- •7.1.1. Таблица векторов в STM32
- •7.2. Разрешение прерываний
- •7.2.1. Линии запроса внешних прерываний и контроллер NVIC
- •7.2.2. Разрешение прерываний в CubeMX
- •7.3. Жизненный цикл прерываний
- •7.4. Уровни приоритета прерываний
- •7.4.1. Cortex-M0/0+
- •7.4.2. Cortex-M3/4/7
- •7.4.3. Установка уровня прерываний в CubeMX
- •7.5. Реентерабельность прерываний
- •8. Универсальные асинхронные последовательные средства связи
- •8.1. Введение в UART и USART
- •8.2. Инициализация UART
- •8.3. UART-связь в режиме опроса
- •8.3.1. Установка консоли последовательного порта в Windows
- •8.3.2. Установка консоли последовательного порта в Linux и MacOS X
- •8.4. UART-связь в режиме прерываний
- •8.5. Обработка ошибок
- •8.6. Перенаправление ввода-вывода
- •9. Управление DMA
- •9.1. Введение в DMA
- •9.1.1. Необходимость DMA и роль внутренних шин
- •9.1.2. Контроллер DMA
- •9.2. Модуль HAL_DMA
- •9.2.1. DMA_HandleTypeDef в HAL для F0/F1/F3/L0/L1/L4
- •9.2.2. DMA_HandleTypeDef в HAL для F2/F4/F7
- •9.2.3. DMA_HandleTypeDef в HAL для L0/L4
- •9.2.4. Как выполнять передачи в режиме опроса
- •9.2.5. Как выполнять передачи в режиме прерываний
- •9.2.8. Разнообразные функции модулей HAL_DMA и HAL_DMA_Ex
- •9.3. Использование CubeMX для конфигурации запросов к DMA
- •10. Схема тактирования
- •10.1. Распределение тактового сигнала
- •10.1.1. Обзор схемы тактирования STM32
- •10.1.1.1. Многочастотный внутренний RC-генератор в семействах STM32L
- •10.1.3.1. Подача тактового сигнала от высокочастотного генератора
- •10.1.3.2. Подача тактового сигнала от 32кГц генератора
- •10.2. Обзор модуля HAL_RCC
- •10.2.1. Вычисление тактовой частоты во время выполнения
- •10.2.2. Разрешение Выхода синхронизации
- •10.2.3. Разрешение Системы защиты тактирования
- •10.3. Калибровка HSI-генератора
- •11. Таймеры
- •11.1. Введение в таймеры
- •11.1.1. Категории таймеров в микроконтроллере STM32
- •11.1.2. Доступность таймеров в ассортименте STM32
- •11.2. Базовые таймеры
- •11.2.1. Использование таймеров в режиме прерываний
- •11.2.2. Использование таймеров в режиме опроса
- •11.2.3. Использование таймеров в режиме DMA
- •11.2.4. Остановка таймера
- •11.3. Таймеры общего назначения
- •11.3.1.1. Режим внешнего тактирования 2
- •11.3.1.2. Режим внешнего тактирования 1
- •11.3.2. Режимы синхронизации ведущего/ведомого таймеров
- •11.3.2.1. Разрешение прерываний, относящихся к триггерной цепи
- •11.3.2.2. Использование CubeMX для конфигурации синхронизации ведущего/ведомого устройств
- •11.3.3. Программная генерация связанных с таймером событий
- •11.3.4. Режимы отсчета
- •11.3.5. Режим захвата входного сигнала
- •11.3.5.1. Использование CubeMX для конфигурации режима захвата входного сигнала
- •11.3.6. Режим сравнения выходного сигнала
- •11.3.6.1. Использование CubeMX для конфигурации режима сравнения выходного сигнала
- •11.3.7. Генерация широтно-импульсного сигнала
- •11.3.7.1. Генерация синусоидального сигнала при помощи ШИМ
- •11.3.7.2. Использование CubeMX для конфигурации режима ШИМ
- •11.3.8. Одноимпульсный режим
- •11.3.8.1. Использование CubeMX для конфигурации одноимпульсного режима
- •11.3.9. Режим энкодера
- •11.3.9.1. Использование CubeMX для конфигурации режима энкодера
- •11.3.10.1. Режим датчика Холла
- •11.3.10.2. Комбинированный режим трехфазной ШИМ и другие функции управления двигателем
- •11.3.10.3. Вход сброса таймера и блокировка регистров таймера
- •11.3.10.4. Предварительная загрузка регистра автоперезагрузки
- •11.3.11. Отладка и таймеры
- •11.4. Системный таймер SysTick
- •12. Аналого-цифровое преобразование
- •12.1. Введение в АЦП последовательного приближения
- •12.2. Модуль HAL_ADC
- •12.2.1. Режимы преобразования
- •12.2.1.1. Режим однократного преобразования одного канала
- •12.2.1.2. Режим сканирования с однократным преобразованием
- •12.2.1.3. Режим непрерывного преобразования одного канала
- •12.2.1.4. Режим сканирования с непрерывным преобразованием
- •12.2.1.5. Режим преобразования инжектированных каналов
- •12.2.1.6. Парный режим
- •12.2.2. Выбор канала
- •12.2.3. Разрядность АЦП и скорость преобразования
- •12.2.4. Аналого-цифровые преобразования в режиме опроса
- •12.2.6. Аналого-цифровые преобразования в режиме DMA
- •12.2.6.1. Многократное преобразование одного канала в режиме DMA
- •12.2.6.3. Непрерывные преобразования в режиме DMA
- •12.2.7. Обработка ошибок
- •12.2.8. Преобразования, управляемые таймером
- •12.2.9. Преобразования, управляемые внешними событиями
- •12.2.10. Калибровка АЦП
- •12.3. Использование CubeMX для конфигурации АЦП
- •13.1. Введение в периферийное устройство ЦАП
- •13.2. Модуль HAL_DAC
- •13.2.1. Управление ЦАП вручную
- •13.2.2. Управление ЦАП в режиме DMA с использованием таймера
- •13.2.3. Генерация треугольного сигнала
- •13.2.4. Генерация шумового сигнала
- •14.1. Введение в спецификацию I²C
- •14.1.1. Протокол I²C
- •14.1.1.1. START- и STOP-условия
- •14.1.1.2. Формат байта
- •14.1.1.3. Кадр адреса
- •14.1.1.4. Биты «Подтверждено» (ACK) и «Не подтверждено» (NACK)
- •14.1.1.5. Кадры данных
- •14.1.1.6. Комбинированные транзакции
- •14.1.1.7. Удержание синхросигнала
- •14.1.2. Наличие периферийных устройств I²C в микроконтроллерах STM32
- •14.2. Модуль HAL_I2C
- •14.2.1.1. Операции I/O MEM
- •14.2.1.2. Комбинированные транзакции
- •14.3. Использование CubeMX для конфигурации периферийного устройства I²C
- •15.1. Введение в спецификацию SPI
- •15.1.1. Полярность и фаза тактового сигнала
- •15.1.2. Управление сигналом Slave Select
- •15.1.3. Режим TI периферийного устройства SPI
- •15.1.4. Наличие периферийных устройств SPI в микроконтроллерах STM32
- •15.2. Модуль HAL_SPI
- •15.2.1. Обмен сообщениями с использованием периферийного устройства SPI
- •15.2.2. Максимальная частота передачи, достижимая при использовании CubeHAL
- •15.3. Использование CubeMX для конфигурации периферийного устройства SPI
- •16. Циклический контроль избыточности
- •16.1. Введение в расчет CRC
- •16.1.1. Расчет CRC в микроконтроллерах STM32F1/F2/F4/L1
- •16.2. Модуль HAL_CRC
- •17. Независимый и оконный сторожевые таймеры
- •17.1. Независимый сторожевой таймер
- •17.1.1. Использование CubeHAL для программирования таймера IWDG
- •17.2. Системный оконный сторожевой таймер
- •17.2.1. Использование CubeHAL для программирования таймера WWDG
- •17.3. Отслеживание системного сброса, вызванного сторожевым таймером
- •17.4. Заморозка сторожевых таймеров во время сеанса отладки
- •17.5. Выбор сторожевого таймера, подходящего для вашего приложения
- •18. Часы реального времени
- •18.1. Введение в периферийное устройство RTC
- •18.2. Модуль HAL_RTC
- •18.2.1. Установка и получение текущей даты/времени
- •18.2.1.1. Правильный способ чтения значений даты/времени
- •18.2.2. Конфигурирование будильников
- •18.2.3. Блок периодического пробуждения
- •18.2.5. Калибровка RTC
- •18.2.5.1. Грубая калибровка RTC
- •18.2.5.2. Тонкая калибровка RTC
- •18.2.5.3. Обнаружение опорного тактового сигнала
- •18.3. Использование резервной SRAM
- •III Дополнительные темы
- •19. Управление питанием
- •19.1. Управление питанием в микроконтроллерах на базе Cortex-M
- •19.2. Как микроконтроллеры Cortex-M управляют рабочим и спящим режимами
- •19.2.1. Переход в/выход из спящих режимов
- •19.2.1.1. «Спящий режим по выходу»
- •19.3. Управление питанием в микроконтроллерах STM32F
- •19.3.1. Источники питания
- •19.3.2. Режимы питания
- •19.3.2.1. Рабочий режим
- •19.3.2.2. Спящий режим
- •19.3.2.3. Режим останова
- •19.3.2.4. Режим ожидания
- •19.3.2.5. Пример работы в режимах пониженного энергопотребления
- •19.4. Управление питанием в микроконтроллерах STM32L
- •19.4.1. Источники питания
- •19.4.2. Режимы питания
- •19.4.2.1. Рабочие режимы
- •19.4.2.2. Спящие режимы
- •19.4.2.2.1. Режим пакетного сбора данных
- •19.4.2.3. Режимы останова
- •19.4.2.4. Режимы ожидания
- •19.4.2.5. Режим выключенного состояния
- •19.4.3. Переходы между режимами питания
- •19.4.4. Периферийные устройства с пониженным энергопотреблением
- •19.4.4.1. LPUART
- •19.4.4.2. LPTIM
- •19.5. Инспекторы источников питания
- •19.6. Отладка в режимах пониженного энергопотребления
- •19.7. Использование калькулятора энергопотребления CubeMX
- •20. Организация памяти
- •20.1. Модель организации памяти в STM32
- •20.1.1. Основы процессов компиляции и компоновки
- •20.2.1. Исследование бинарного ELF-файла
- •20.2.2. Инициализация секций .data и .bss
- •20.2.2.1. Пара слов о секции COMMON
- •20.2.3. Секция .rodata
- •20.2.4. Области Стека и Кучи
- •20.2.5. Проверка размера Кучи и Стека на этапе компиляции
- •20.2.6. Различия с файлами скриптов инструментария
- •20.3. Как использовать CCM-память
- •20.3.1. Перемещение таблицы векторов в CCM-память
- •20.4.1. Программирование MPU с использованием CubeHAL
- •21. Управление Flash-памятью
- •21.1. Введение во Flash-память STM32
- •21.2. Модуль HAL_FLASH
- •21.2.1. Разблокировка Flash-памяти
- •21.2.2. Стирание Flash-памяти
- •21.2.3. Программирование Flash-памяти
- •21.3. Байты конфигурации
- •21.3.1. Защита от чтения Flash-памяти
- •21.4. Дополнительные памяти OTP и EEPROM
- •21.5. Задержка чтения Flash-памяти и ускоритель ART™ Accelerator
- •21.5.1. Роль TCM-памятей в микроконтроллерах STM32F7
- •22. Процесс начальной загрузки
- •22.1.1. Программное физическое перераспределение памяти
- •22.1.2. Перемещение таблицы векторов
- •22.1.3. Запуск микропрограммы из SRAM с помощью инструментария GNU MCU Eclipse
- •22.2. Встроенный загрузчик
- •22.2.1. Запуск загрузчика из встроенного программного обеспечения
- •22.2.2. Последовательность начальной загрузки в инструментарии GNU MCU Eclipse
- •22.3. Разработка пользовательского загрузчика
- •22.3.2. Как использовать инструмент flasher.py
- •23. Запуск FreeRTOS
- •23.1. Введение в концепции, лежащие в основе ОСРВ
- •23.2.1. Структура файлов с исходным кодом FreeRTOS
- •23.2.1.2. Как импортировать FreeRTOS с использованием CubeMX и CubeMXImporter
- •23.3. Управление потоками
- •23.3.1. Состояния потоков
- •23.3.2. Приоритеты потоков и алгоритмы планирования
- •23.3.3. Добровольное освобождение от управления
- •23.3.4. Холостой поток idle
- •23.4. Выделение памяти и управление ею
- •23.4.1. Модель динамического выделения памяти
- •23.4.1.1. heap_1.c
- •23.4.1.2. heap_2.c
- •23.4.1.3. heap_3.c
- •23.4.1.4. heap_4.c
- •23.4.1.5. heap_5.c
- •23.4.2. Модель статического выделения памяти
- •23.4.3. Пулы памяти
- •23.4.4. Обнаружение переполнения стека
- •23.5. Примитивы синхронизации
- •23.5.1. Очереди сообщений
- •23.5.2. Cемафоры
- •23.5.3. Сигналы потоков
- •23.6. Управление ресурсами и взаимное исключение
- •23.6.1. Мьютексы
- •23.6.2. Критические секции
- •23.6.3. Обработка прерываний совместно с ОСРВ
- •23.7. Программные таймеры
- •23.7.1. Как FreeRTOS управляет таймерами
- •23.8. Пример из практики: Управление энергосбережением с ОСРВ
- •23.8.1. Перехват холостого потока idle
- •23.8.2. Бестиковый режим во FreeRTOS
- •23.9. Возможности отладки
- •23.9.1. Макрос configASSERT()
- •23.9.2. Статистика среды выполнения и информация о состоянии потоков
- •23.10. Альтернативы FreeRTOS
- •23.10.1. ChibiOS
- •23.10.2. ОС Contiki
- •23.10.3. OpenRTOS
- •24. Продвинутые методы отладки
- •24.1. Введение в исключения отказов Cortex-M
- •24.1.1.1. Как инструментарий GNU MCU Eclipse обрабатывает исключения отказов
- •24.1.1.2. Как интерпретировать содержимое регистра LR при переходе в исключение
- •24.1.2. Исключения отказов и их анализ
- •24.2.1. Представление Expressions
- •24.2.1.1. Мониторы памяти
- •24.2.2. Точки наблюдения
- •24.2.3. Режим Instruction Stepping Mode
- •24.2.4. Keil Packs и представление Peripheral Registers
- •24.2.5. Представление Core Registers
- •24.3. Средства отладки от CubeHAL
- •24.4. Внешние отладчики
- •24.4.1. Использование SEGGER J-Link для отладчика ST-LINK
- •24.4.2. Использование интерфейса ITM и трассировка SWV
- •24.5. STM Studio
- •24.6. Одновременная отладка двух плат Nucleo
- •25. Файловая система FAT
- •25.1. Введение в библиотеку FatFs
- •25.1.1. Использование CubeMX для включения в ваши проекты библиотеки FatFs
- •25.1.2. Наиболее важные структуры и функции FatFs
- •25.1.2.1. Монтирование файловой системы
- •25.1.2.2. Открытие файлов
- •25.1.2.3. Чтение и запись файла
- •25.1.2.4. Создание и открытие каталога
- •25.1.3. Как сконфигурировать библиотеку FatFs
- •26. Разработка IoT-приложений
- •26.2. Ethernet контроллер W5500
- •26.2.1. Как использовать шилд W5500 и модуль ioLibrary_Driver
- •26.2.1.1. Конфигурирование интерфейса SPI
- •26.2.1.2. Настройка буферов сокетов и сетевого интерфейса
- •26.2.2. API-интерфейсы сокетов
- •26.2.2.1. Управление сокетами в режиме TCP
- •26.2.2.2. Управление сокетами в режиме UDP
- •26.2.3. Перенаправление ввода-вывода на сокет TCP/IP
- •26.2.4. Настройка HTTP-сервера
- •26.2.4.1. Веб-осциллограф
- •27. Начало работы над новым проектом
- •27.1. Проектирование оборудования
- •27.1.1. Послойная разводка печатной платы
- •27.1.2. Корпус микроконтроллера
- •27.1.3. Развязка выводов питания
- •27.1.4. Тактирование
- •27.1.5. Фильтрация вывода сброса RESET
- •27.1.6. Отладочный порт
- •27.1.7. Режим начальной загрузки
- •27.1.8. Обратите внимание на совместимость с выводами…
- •27.1.9. …и на выбор подходящей периферии
- •27.1.10. Роль CubeMX на этапе проектирования платы
- •27.1.11. Стратегии разводки платы
- •27.2. Разработка программного обеспечения
- •27.2.1. Генерация бинарного образа для производства
- •Приложение
- •Принудительный сброс микроконтроллера из микропрограммы
- •B. Руководство по поиску и устранению неисправностей
- •Проблемы с установкой GNU MCU Eclipse
- •Проблемы, связанные с Eclipse
- •Eclipse не может найти компилятор
- •Eclipse постоянно прерывается при выполнении каждой инструкции во время сеанса отладки
- •Пошаговая отладка очень медленная
- •Микропрограмма работает только в режиме отладки
- •Проблемы, связанные с STM32
- •Микроконтроллер не загружается корректно
- •Невозможно загрузить микропрограмму или отладить микроконтроллер
- •C. Схема выводов Nucleo
- •Nucleo-F446RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F411RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F410RB
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F401RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F334R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F303RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F302R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F103RB
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F091RC
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F072RB
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F070RB
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F030R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-L476RG
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-L152RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-L073R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-L053R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •D. Корпусы STM32
- •LFBGA
- •LQFP
- •TFBGA
- •TSSOP
- •UFQFPN
- •UFBGA
- •VFQFP
- •WLCSP
- •E. Изменения книги
- •Выпуск 0.1 – Октябрь 2015
- •Выпуск 0.2 – 28 октября 2015
- •Выпуск 0.2.1 – 31 октября 2015
- •Выпуск 0.2.2 – 1 ноября 2015
- •Выпуск 0.3 – 12 ноября 2015
- •Выпуск 0.4 – 4 декабря 2015
- •Выпуск 0.5 – 19 декабря 2015
- •Выпуск 0.6 – 18 января 2016
- •Выпуск 0.6.1 – 20 января 2016
- •Выпуск 0.6.2 – 30 января 2016
- •Выпуск 0.7 – 8 февраля 2016
- •Выпуск 0.8 – 18 февраля 2016
- •Выпуск 0.8.1 – 23 февраля 2016
- •Выпуск 0.9 – 27 марта 2016
- •Выпуск 0.9.1 – 28 марта 2016
- •Выпуск 0.10 – 26 апреля 2016
- •Выпуск 0.11 – 27 мая 2016
- •Выпуск 0.11.1 – 3 июня 2016
- •Выпуск 0.11.2 – 24 июня 2016
- •Выпуск 0.12 – 4 июля 2016
- •Выпуск 0.13 – 18 июля 2016
- •Выпуск 0.14 – 12 августа 2016
- •Выпуск 0.15 – 13 сентября 2016
- •Выпуск 0.16 – 3 октября 2016
- •Выпуск 0.17 – 24 октября 2016
- •Выпуск 0.18 – 15 ноября 2016
- •Выпуск 0.19 – 29 ноября 2016
- •Выпуск 0.20 – 28 декабря 2016
- •Выпуск 0.21 – 29 января 2017
- •Выпуск 0.22 – 2 мая 2017
- •Выпуск 0.23 – 20 июля 2017
- •Выпуск 0.24 – 11 декабря 2017
- •Выпуск 0.25 – 3 января 2018
- •Выпуск 0.26 – 7 мая 2018
I2C |
425 |
14.2.1.1. Операции I/O MEM
Протокол, используемый EEPROM 24LCxx в действительности является общим для всех устройств I²C, которые имеют адресуемые в памяти регистры чтения/записи. Например, многие датчики I²C, такие как HTS221 от ST, используют один и тот же протокол. По этой причине инженеры ST уже реализовали определенные процедуры в CubeHAL, которые выполняют ту же работу, что и Read_From_24LCxx() и Write_To_24LCxx(), только лучше и быстрее. Функции:
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
позволяют сохранять и извлекать данные из устройств I²C с адресуемой памятью с одним заметным отличием: функция HAL_I2C_Mem_Write() не предназначена для ожидания завершения цикла записи, как мы это делали в предыдущем примере в строке 107. Но и для этой операции HAL предоставляет специальную и более переносимую процедуру:
HAL_StatusTypeDef HAL_I2C_IsDeviceReady(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout);
Данная функция принимает максимальное количество попыток опроса Trials перед возвратом условия ошибки, но если мы передадим функции HAL_MAX_DELAY в качестве значения Timeout, то в аргумент Trials можно передать значение 1. Когда опрошенное устройство I²C готово, функция возвращает HAL_OK. В противном случае она возвращает значение HAL_BUSY.
Итак, функция main(), показанная ранее, может быть перестроена следующим образом:
14int main(void) {
15char wmsg[] ="We love STM32!";
16char rmsg[20];
17
18HAL_Init();
19Nucleo_BSP_Init();
21 |
MX_I2C1_Init(); |
22 |
|
23 |
HAL_I2C_Mem_Write(&hi2c1, 0xA0, 0x1AAA, I2C_MEMADD_SIZE_16BIT, (uint8_t*)wmsg, |
24 |
strlen(wmsg)+1, HAL_MAX_DELAY); |
25 |
while(HAL_I2C_IsDeviceReady(&hi2c1, 0xA0, 1, HAL_MAX_DELAY) != HAL_OK); |
26 |
|
27 |
HAL_I2C_Mem_Read(&hi2c1, 0xA0, 0x1AAA, I2C_MEMADD_SIZE_16BIT, (uint8_t*)rmsg, |
28 |
strlen(wmsg)+1, HAL_MAX_DELAY); |
I2C |
426 |
29
30if(strcmp(wmsg, rmsg) == 0) {
31while(1) {
32HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
33HAL_Delay(100);
34}
35}
36
37while(1);
38}
Вышеуказанные API-интерфейсы работают в режиме опроса, но CubeHAL также предоставляет соответствующие процедуры для выполнения транзакций в режиме прерываний и DMA. Как обычно, эти другие API-интерфейсы имеют аналогичную сигнатуру функции, с одним только отличием: функциями обратного вызова, используемыми для оповещения об окончании передачи, являются и
HAL_I2C_MemRxCpltCallback(), как показано в таблице 3.
14.2.1.2. Комбинированные транзакции
Последовательность передачи при операции чтения памяти EEPROM 24LCxx относится к категории комбинированных транзакций. Перед инвертированием направления передачи I²C (от записи к чтению) используется RESTART-условие. В первом примере мы смогли использовать две отдельные транзакции внутри Read_From_24LCxx(), потому что EEPROM 24LCxx спроектированы для подобной работы. Это возможно благодаря внутреннему счетчику адресов: первая транзакция устанавливает счетчик адресов на желаемую ячейку; вторая, выполненная в режиме чтения, извлекает данные из EEPROM, начиная с этой ячейки. Однако это не только снижает максимально достижимую пропускную способность, но, что более важно, часто приводит к непереносимому коду: существуют некоторые устройства I²C, которые строго придерживаются протокола I²C и реализуют комбинированные транзакции в соответствии со спецификацией, используя RESTARTусловие (поэтому они не совместимы с использованием STOP-условия в середине).
CubeHAL предоставляет две специальные процедуры для обработки комбинированных транзакций или, как они называются в CubeHAL, последовательных передач (sequential transmissions):
HAL_I2C_Master_Sequential_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size,uint32_t XferOptions);
HAL_I2C_Master_Sequential_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t XferOptions);
По сравнению с другими процедурами, которые мы рассмотрели ранее, единственным важным параметром, который здесь следует выделить, является XferOptions. Он может принимать одно из значений, указанных в таблице 4, и он используется для управления генерацией START-/RESTART-/STOP-условий в одной транзакции. Обе функции работают следующим образом. Предположим, что мы хотим прочитать n-байт из EEPROM 24LCxx. Согласно протоколу I²C, мы должны выполнить следующие операции
(см. рисунок 10):
I2C |
427 |
1.мы должны начать новую транзакцию в режиме записи, выдав START-условие, за которым следует адрес ведомого устройства;
2.затем мы передаем два байта, содержащие MSB и LSB части адреса ячейки памяти;
3.после мы выдаем RESTART-условие и передаем адрес ведомого устройства с последним битом, установленным в 1, чтобы начать транзакцию чтения.
4.ведомое устройство начинает посылать побайтно данные до тех пор, пока мы не завершим транзакцию, выдав NACK или STOP-условие.
Таблица 4: Значения параметра XferOptions для управления генерацией START-/RESTART-/STOP- условий
Вариант передачи |
Описание |
|
|
I2C_FIRST_FRAME |
Этот вариант позволяет генерировать только START-условие, |
|
не генерируя окончательное STOP-условие в конце передачи. |
I2C_NEXT_FRAME |
Этот вариант позволяет генерировать RESTART-условие перед |
|
передачей данных при изменении направления передачи (то |
|
есть мы вызываем HAL_I2C_Master_Sequential_Transmit_IT() по- |
|
сле HAL_I2C_Master_Sequential_Receive_IT() или наоборот), или |
|
он позволяет управлять только новыми данными для пере- |
|
дачи без изменения направления передачи и без окончатель- |
|
ного STOP-условия в обоих случаях. |
I2C_LAST_FRAME |
Этот вариант позволяет генерировать RESTART-условие перед |
|
передачей данных при изменении направления передачи (то |
|
есть мы вызываем HAL_I2C_Master_Sequential_Transmit_IT() по- |
|
сле HAL_I2C_Master_Sequential_Receive_IT() или наоборот), или |
|
он позволяет управлять только новыми данными для пере- |
|
дачи без изменения направления передачи и с окончатель- |
|
ным STOP-условием в обоих случаях. |
I2C_FIRST_AND_LAST_FRAME |
Последовательная передача не используется. Обе процедуры |
|
работают одинаково для функций HAL_I2C_Master_Transmit_IT() |
|
и HAL_I2C_Master_Receive_IT(). |
Используя процедуры последовательной передачи, мы можем действовать следующим образом:
1.вызываем процедуру HAL_I2C_Master_Sequential_Transmit_IT(), передавая адрес
ведомого устройства и два байта, образующие адрес ячейки памяти; вызываем функцию, передавая значение I2C_FIRST_FRAME, чтобы она генерировала STARTусловие без выдачи STOP-условия после отправки двух байт;
2.также вызываем процедуру HAL_I2C_Master_Sequential_Receive_IT(), передавая ад-
рес ведомого устройства, указатель на буфер, используемый для хранения считанных байт, количество считываемых байт из EEPROM и значение I2C_LAST_FRAME, чтобы функция генерировала RESTART-условие и завершала
транзакцию в конце передачи, выдавая STOP-условие.
На момент написания данной главы процедуры последовательной передачи существовали только в версии для режима прерываний. Мы не будем здесь анализировать пример использования, потому что мы будем их широко использовать (вместе с теми, которые используются для разработки приложений с ведомыми I²C-устройствами) в следующем параграфе.
I2C |
428 |
Прочитайте внимательно
На момент написания данной главы последние выпуски CubeHAL для семейств F1 и L0 не предоставляли процедуры последовательной передачи. Я думаю, что ST активно работает над этим, и следующие выпуски HAL должны предоставить их.
По той же причине владельцы плат Nucleo-F103RB и Nucleo-L0XX не смогут выполнить примеры, касающиеся использования периферийного устройства I²C в режиме ведомого.
14.2.1.3.Замечание о конфигурации тактирования в семействах
STM32F0/L0/L4
Всемействах STM32F0/L0 можно выбрать разные источники тактового сигнала для периферийного устройства I2C1. Это связано с тем, что в данных семействах периферийное устройство I2C1 способно работать даже в некоторых режимах пониженного энергопотребления, что позволяет активировать микроконтроллер, когда I²C работает в режиме ведомого и сконфигурированный адрес ведомого устройства попадает на шину. Обратитесь к представлению Clock Configuration в CubeMX для получения дополнительной информации об этом.
Вмикроконтроллерах STM32L4 можно выбрать источник тактового сигнала для всех периферийных устройств I²C.
14.2.2.Использование периферийного устройства I²C в
режиме ведомого
В настоящее время на рынке продается множество модулей типа System-on-Board (SoB или одноплатные системы). Обычно это небольшие печатные платы с уже установленными несколькими ИС, специализирующиеся на выполнении какой-либо конкретной задачи. Модули GPRS и GPS или многодатчиковые платы являются примерами модулей SoB. Эти модули затем припаиваются к основной плате благодаря тому, что на их боковых сторонах открытые паяемые контакты, также известные как «зубчатые отверстия
(castellated vias)» или «зубцы (castellations)». На рисунке 11 показан модуль INEMO-M1
от ST, который представляет собой интегрированный и программируемый модуль с STM32F103 и двумя высокоинтегрированными MEMS датчиками (6-осевой электронный цифровой компас и 3-осевой цифровой гироскоп).
Рисунок 11: Модуль INEMO-M1 от ST
I2C |
429 |
Микроконтроллер на подобных платах обычно поставляется с предварительно запрограммированной микропрограммой, которая специализируется на выполнении четко поставленной задачи. Плата хоста также содержит другую программируемую ИС, которой может быть другой микроконтроллер или что-то подобное. Основная плата взаимодействует с SoB, используя хорошо известный протокол связи, которым обычно являются UART, шина CAN, SPI или шина I²C. По этой причине достаточно часто устройства STM32 программируют так, чтобы они работали в режиме ведомого I²C-устройства.
CubeHAL предоставляет весь необходимый инструментарий для простой разработки приложений с ведомыми I²C-устройствами. Процедуры для операций с ведомыми устройствами идентичны тем, которые используются для программирования периферийных устройств I²C в режиме ведущего. Например, следующие процедуры используются для передачи/приема данных в режиме прерываний, когда периферийное устройство I²C используется в режиме ведомого:
HAL_StatusTypeDef HAL_I2C_Slave_Transmit_IT(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_I2C_Slave_Receive_IT(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size);
Точно так же процедуры обратного вызова, вызываемые в конце передачи/приема данных, выглядят следующим образом:
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c); void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c);
Теперь рассмотрим полный пример, который показывает, как разрабатывать приложения с ведомыми I²C-устройствами с использованием CubeHAL. Мы реализуем своего рода цифровой датчик температуры с интерфейсом I²C, похожий на большинство цифровых датчиков температуры, представленных на рынке (например, популярный TMP275 от TI и HT221 от ST). Этот «датчик» будет предоставлять только три регистра:
•регистр WHO_AM_I, используемый кодом ведущего устройства для проверки правильности работы интерфейса I²C; этот регистр возвращает фиксированное значение 0xBC.
•два связанных с температурой регистра, называемые TEMP_OUT_INT и TEMP_OUT_FRAC, которые содержат целую и дробную часть полученной температуры; например, если измеренное значение температуры равно 27,34°C, то регистр TEMP_OUT_INT будет содержать значение 27, а TEMP_OUT_FRAC – значение 34.
Рисунок 12: Протокол I²C, используемый для чтения внутреннего регистра нашего ведомого устройства
Наш датчик будет разработан для ответа на довольно простой протокол, основанный на комбинированных транзакциях, который показан на рисунке 12. Как видите, единственное заметное отличие от протокола, используемого в EEPROM 24LCxx при доступе к памяти в режиме произвольного чтения, – это размер регистра памяти, который в данном случае составляет всего один байт.
I2C |
430 |
В примере представлены реализации как «ведомого», так и «ведущего»: макрос SLAVE_BOARD, определенный на уровне проекта, управляет компиляцией двух частей. Пример требует двух плат Nucleo17.
Имя файла: src/main-ex2.c
15 volatile uint8_t transferDirection, transferRequested;
16
17#define TEMP_OUT_INT_REGISTER 0x0
18#define TEMP_OUT_FRAC_REGISTER 0x1
19 |
define WHO_AM_I_REGISTER |
0xF |
20 |
define WHO_AM_I_VALUE |
0xBC |
21 |
define TRANSFER_DIR_WRITE |
0x1 |
22 |
define TRANSFER_DIR_READ |
0x0 |
23 |
define I2C_SLAVE_ADDR |
0x33 |
24 |
|
|
25int main(void) {
26char uartBuf[20];
27uint8_t i2cBuf[2];
28float ftemp;
29int8_t t_frac, t_int;
31HAL_Init();
32Nucleo_BSP_Init();
34 |
MX_I2C1_Init(); |
35 |
|
36#ifdef SLAVE_BOARD
37uint16_t rawValue;
38uint32_t lastConversion;
40MX_ADC1_Init();
41HAL_ADC_Start(&hadc1);
43while(1) {
44HAL_I2C_EnableListen_IT(&hi2c1);
45while(!transferRequested) {
46if(HAL_GetTick() - lastConversion > 1000L) {
47HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
49rawValue = HAL_ADC_GetValue(&hadc1);
50ftemp = ((float)rawValue) / 4095 * 3300;
51ftemp = ((ftemp - 760.0) / 2.5) + 25;
17 К сожалению, когда я начал разрабатывать данный пример, я подумал, что можно использовать только одну плату, соединяющую выводы одного периферийного устройства I²C с выводами другого периферийного устройства I²C (например, выводы I2C1, напрямую подключенные к выводам I2C3), но после многих трудностей я пришел к выводу, что периферийные устройства I²C в STM32 не являются «действительно асинхронными», и невозможно использовать два периферийных устройства I²C одновременно. Таким образом, для запуска этих примеров вам понадобятся две платы Nucleo или только одна Nucleo и другая отладочная плата: в этом случае вам необходимо соответствующим образом перестроить часть ведущего устройства.
I2C |
431 |
53t_int = ftemp;
54t_frac = (ftemp - t_int)*100;
56sprintf(uartBuf, "Temperature: %f\r\n", ftemp);
57HAL_UART_Transmit(&huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY);
59sprintf(uartBuf, "t_int: %d - t_frac: %d\r\n", t_frac, t_int);
60HAL_UART_Transmit(&huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY);
62lastConversion = HAL_GetTick();
63}
64}
66transferRequested = 0;
68if(transferDirection == TRANSFER_DIR_WRITE) {
69/* Ведущее устройство отправляет адрес регистра */
70HAL_I2C_Slave_Sequential_Receive_IT(&hi2c1, i2cBuf, 1, I2C_FIRST_FRAME);
71while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_LISTEN);
73switch(i2cBuf[0]) {
74case WHO_AM_I_REGISTER:
75i2cBuf[0] = WHO_AM_I_VALUE;
76break;
77case TEMP_OUT_INT_REGISTER:
78i2cBuf[0] = t_int;
79break;
80case TEMP_OUT_FRAC_REGISTER:
81i2cBuf[0] = t_frac;
82break;
83default:
84i2cBuf[0] = 0xFF;
85}
86
87HAL_I2C_Slave_Sequential_Transmit_IT(&hi2c1, i2cBuf, 1, I2C_LAST_FRAME);
88while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
89}
90}
Наиболее значимая часть функции main() начинается со строки 44. Процедура HAL_I2C_EnableListen_IT() разрешает все прерывания, связанные с периферийным устройством I²C. Это означает, что новое прерывание сработает, когда ведущее устройство установит адрес ведомого устройства (который определяется макросом
I2C_SLAVE_ADDR). Процедуры HAL_I2C_EV_IRQHandler() будут автоматически вызывать
функцию HAL_I2C_AddrCallback(), которую мы проанализируем позже.
Затем функция main() начинает выполнять аналого-цифровое преобразование внутреннего датчика температуры каждую секунду с разделением полученной температуры (сохраненной в переменную ftemp) на два 8-разрядных целых числа: t_int и t_frac. Они представляют собой целую и дробную части температуры. Функция main() временно
I2C |
432 |
останавливает аналого-цифровое преобразование, как только переменная transferRequested становится равной 1: эта глобальная переменная устанавливается
функцией HAL_I2C_AddrCallback() вместе с переменной transferDirection, которая содер-
жит направление передачи (чтение/запись) транзакции I²C.
Если ведущее устройство запускает новую транзакцию в режиме записи, это означает, что оно передает адрес регистра. Затем в строке 70 вызывается функция HAL_I2C_Slave_Sequential_Receive_IT(): это приведет к тому, что адрес регистра будет получен от ведущего устройства. Поскольку функция работает в режиме прерываний, нам нужен способ дождаться завершения передачи. HAL_I2C_GetState() возвращает внутреннее состояние HAL, которое равно HAL_I2C_STATE_BUSY_RX_LISTEN до завершения передачи. Когда оно происходит, состояние HAL возвращается к HAL_I2C_STATE_LISTEN, и мы можем продолжить, передав ведущему устройству содержимое требуемого регистра.
Данное действие выполняется в строке 87, где вызывается функция HAL_I2C_Slave_Sequential_Transmit_IT(): функция инвертирует направление передачи и отправляет ведущему устройству содержимое требуемого регистра. Сложная конструкция представлена
встроке 88. Здесь мы простаиваем до тех пор, пока состояние периферийного устройства I²C не станет равным HAL_I2C_STATE_READY. Почему мы не проверяем состояние периферийного устройства на соответствие состоянию HAL_I2C_STATE_LISTEN, как мы это делали
встроке 71? Чтобы понять этот аспект, нам нужно запомнить важную особенность комбинированных транзакций. Когда транзакция инвертирует направление передачи, ведущее устройство начинает подтверждать каждый отправленный байт. Помните, что только ведущее устройство знает, как долго длится транзакция, и только оно решает, когда остановить транзакцию. В комбинированных транзакциях ведущее устройство завершает передачу от ведомого к ведущему, выполняя NACK, что заставляет ведомое устройство выполнить STOP-условие. С точки зрения периферийного устройства I²C STOP-условие заставляет периферийное устройство выйти из режима прослушивания (технически говоря, оно генерирует условие прерывания, и если вы реализуете обратный вызов HAL_I2C_AbortCpltCallback(), то сможете отслеживать, когда это происходит), и по этой причине нам нужно проверять состояние HAL_I2C_STATE_READY и снова переводить периферийное устройство в режим прослушивания (listen mode) в строке 44.
Имя файла: src/main-ex2.c
92#else // Плата ведущего устройства
93i2cBuf[0] = WHO_AM_I_REGISTER;
94HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf,
95 |
1, I2C_FIRST_FRAME); |
96 |
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY); |
97 |
|
98 |
HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf, |
99 |
1, I2C_LAST_FRAME); |
100 |
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY); |
101 |
|
102sprintf(uartBuf, "WHO AM I: %x\r\n", i2cBuf[0]);
103HAL_UART_Transmit(&huart2, (uint8_t*) uartBuf, strlen(uartBuf), HAL_MAX_DELAY);
105i2cBuf[0] = TEMP_OUT_INT_REGISTER;
106HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf,
107 |
1, I2C_FIRST_FRAME); |
108 |
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY); |
|
I2C |
433 |
109 |
|
|
110 |
HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, (uint8_t*)&t_int, |
|
111 |
1, I2C_LAST_FRAME); |
|
112 |
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY); |
|
113 |
|
|
114i2cBuf[0] = TEMP_OUT_FRAC_REGISTER;
115HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf,
116 |
1, I2C_FIRST_FRAME); |
117 |
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY); |
118 |
|
119 |
HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, (uint8_t*)&t_frac, |
120 |
1, I2C_LAST_FRAME); |
121 |
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY); |
122 |
|
123ftemp = ((float)t_frac)/100.0;
124ftemp += (float)t_int;
125
126sprintf(uartBuf, "Temperature: %f\r\n", ftemp);
127HAL_UART_Transmit(&huart2, (uint8_t*) uartBuf, strlen(uartBuf), HAL_MAX_DELAY);
129#endif
131while (1);
132}
Наконец, важно подчеркнуть, что реализация «части ведомого устройства» все еще недостаточно надежна. На самом деле, мы должны разобраться со всеми возможными неправильными случаями, которые могут произойти. Например, ведущее устройство может разорвать соединение в середине двух транзакций. Это сильно усложнит пример, и его реализация остается читателю.
«Часть ведущего устройства» примера начинается со строки 92. Код довольно прост.
Здесь мы используем функцию HAL_I2C_Master_Sequential_Transmit_IT() для запуска
комбинированной транзакции и HAL_I2C_Master_Sequential_Receive_IT() для получения содержимого требуемого регистра от ведомого устройства. Затем целая и дробная части температуры снова объединяются в число типа float, и полученная температура отправляется по UART2.
Имя файла: src/main-ex2.c
134void I2C1_EV_IRQHandler(void) {
135HAL_I2C_EV_IRQHandler(&hi2c1);
136}
137
138void I2C1_ER_IRQHandler(void) {
139HAL_I2C_ER_IRQHandler(&hi2c1);
140}
141
142void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t \
143AddrMatchCode) {
144UNUSED(AddrMatchCode);
145