Всех снова приветствую, и сегодня будет еще одна статья из цикла "по запросам трудящихся" 👍 Из названия статьи и превью уже понятно, что разговор пойдет о подключении и работе с семисегментными индикаторами. Собственно, вопросы касаются в большей степени программных аспектов, поэтому во второй части прямо по ходу написания текста статьи я реализую свой вариант библиотеки для работы с упомянутыми 7-сегментными индикаторами. Но начинаем, по устоявшейся традиции, с базовых теоретических сведений.
Принцип работы семисегментного индикатора.
Что в целом из себя представляет 7-сегментный индикатор? Идея тут проста – просто упорядоченный набор светодиодов. Причем упорядоченность касается их физического расположения друг относительно друга:
Каждый из светодиодов представляет из себя отдельный сегмент, соответственно, имеем как раз 7 сегментов. При этом каждому сегменту ставится в соответствие условное буквенное обозначение – a, b, c и т. д. Поместим все это в пластиковый корпус и, наклеив наклейку, получаем законченный компонент:
Включая определенный набор диодов, можем получить наглядное отображение информации, к примеру, в виде цифры 7:
Аналогичным образом и для любой другой цифры:
Или для любого символа, варианты ограничены только количеством сегментов, конкретной целью и фантазией:
Помимо этих семи сегментов почти всегда присутствует еще один, отвечающий за отображение точки:
В комплексе такой элемент представляет из себя один разряд, позволяющий отобразить одну цифру или символ. В продаже же, в основном, имеются одно-, двух-, трех- и четырехразрядные индикаторы:
По итогу, имеем возможность полноценного отображения некой информации, например температуры, давления, да и чего угодно в целом. Как видите, суть проста, понятна и логична.
Подключение семисегментного индикатора.
Электрически данная концепция реализуется также максимально просто: одним из выводов все светодиоды соединяются в одной точке, вторые же подключаются отдельно. Что в совокупности дает нам два варианта, с общим анодом:
И с общим катодом:
Управление тогда будет выглядеть следующим образом, для общего анода:
- подаем на общий вывод (аноды) положительное напряжение
- катоды же нужных сегментов заземляем
Для общего катода то же самое, за исключением полярности:
- подаем на общий вывод (катоды) 0 В
- а на аноды сегментов положительное значение напряжения
Естественно, нужно учесть параметры конкретных диодов в конкретном же используемом индикаторе. Допустим, из документации узнаем, что прямой ток диодов – 20 мА при напряжении на диоде 2 В. При осуществлении управления напрямую с вывода микроконтроллера, например STM32, напряжение будет составлять 3.3 В, что очевидно не равно требуемым 2 В. Поэтому в цепь добавляется резистор:
И номинал его рассчитывается следующим образом. Напряжение на диоде должно быть равно 2 В, с порта контроллера имеем 3.3 В, значит избыточные 1.3 В должны упасть именно на резисторе.
При этом ток через диод, а вместе с ним и ток через резистор, составит 20 мА. Поэтому по закону Ома спокойно рассчитываем необходимое значение сопротивления:
R = \frac{U_R}{I_R} = \frac{1.3 \medspace В}{20 \medspace мА} = 65 \medspace Ом
На этом расчетная деятельность закончена.
Кроме того, при необходимости, управление может осуществляться, как вариант, через транзисторные ключи. Собственно, все это не влияет на идею, которая заключается в том, что зажигание диода производится подачей положительного напряжения между его анодом и катодом.
Динамическая индикация.
Постепенно начинаем перемещаться к практическим нюансам. И первым, что приходит на ум, является тот факт, что для управления одним разрядом потребуется целых девять выводов микроконтроллера. Будем для наглядности рассматривать схему с общим катодом, никаких ограничений это не накладывает. Тогда получаем эти самые девять требуемых сигналов:
- катод диодов – 1 штука
- сегменты для отображения цифры/символа – 7 штук
- сегмент десятичной точки – 1 штука
Нехитрые манипуляции в виде суммирования дают именно девять сигналов для полноценного управления. И это только для одного разряда. Если разрядов будет 4, то это уже 36 выводов микроконтроллера, что, мягко говоря, чуть больше, чем очень много.
Решение же данной проблемы заключается в использовании динамической индикации. Принцип заключается в том, что в каждый момент времени светится только одна цифра. Допустим, нам нужно вывести на индикатор число "1971", действуем так:
- Зажигаем "1" на месте первого разряда.
- Ожидаем некоторое время, пусть 1 миллисекунду. В этом временном интервале у нас горит "1", остальные же три разряда выключены полностью.
- Гасим "1" в первом разряде и зажигаем "9" на второй позиции.
- Снова ожидаем в таком состоянии миллисекунду.
- Повторяем аналогичные действия для оставшихся цифр "7" и "1".
И в итоге за счет того, что смена активных разрядов осуществляется быстро, это изменение остается невидимым для глаза, и поэтому визуально выглядит так, будто цифры горят одновременно. Что и дает нам отображение четырехзначного числа. Графическая иллюстрация динамической индикации:
И теперь нам требуется не 36, а только 12 выводов для управления 4-х разрядным индикатором. Рассмотрим более подробно на конкретном примере такого индикатора:
Поскольку динамическая индикация является классическим способом управления такими девайсами, то зачастую на этапе производства выводы сегментов для разных разрядов соединяются внутри индикатора:
Здесь у нас вариант с общим анодом. Суммарно этот семисегментный индикатор содержит в себе 4 разряда, по 8 сегментов каждый (7 + 1 для точки) и 12 выводов для управления всем хозяйством. Восемь сегментов каждого из разрядов соединены анодами и каждый из них, в свою очередь, выведен на выходной разъем. В данном случае это пины с номерами 12, 9, 8 и 6. Сегменты же разных разрядов соединены катодами между собой. Так точка 1-го разряда, соединена с точками 2-го, 3-го и 4-го разрядов, и этот сигнал выходит на 3-й контакт разъема:
Точно так же и для остальных катодов каждого из светодиодов. Возвращаемся к динамической индикации, которая на этом конкретном примере трансформируется в следующий алгоритм:
- Подаем на 12-й вывод положительное напряжение, 9-й, 8-й, 6-й заземляем. Таким образом, мы "активируем" только первый разряд.
- Для того, чтобы зажечь единицу нужно подать на катоды диодов b и c 0 В (выводы 7 и 4). На остальные же катоды подаем то же напряжение, что и на анод. В итоге гореть будут только светодиоды b и c первого разряда.
- Ожидаем некоторое непродолжительное время.
- Далее наша задача вывести "9" на второй позиции, потушив все остальные разряды. Для этого по той же схеме – подаем на 9-й вывод положительное напряжение, 12-й, 8-й, 6-й заземляем.
- Теперь у нас "активирован" второй разряд и только он.
- Для отображения "9" подаем на катоды диодов a, b, c, d, f, g - 0 В, на катод диода e – то же напряжение, что и на его анод, дабы он оставался в покое.
- Все, в дальнейшем те же действия для 3-го и 4-го разрядов и все это повторяем циклично. Результатом будет отображение нужного числа.
В итоге, управление осуществляется 12-ю выводами микроконтроллера, что тоже немало, но, как минимум, меньше 36-ти. Есть, конечно же, в продаже и 7-сегментные индикаторы, в которых на разъем выведены все 36 сигналов, соответственно, они не закорочены между собой внутри. Вот, например:
Поэтому при выборе следует исходить, в первую очередь, из конкретной задачи и требований. Более наглядно мы все описанное увидим в программной части статьи, к которой и переходим.
Работа с 7-сегментным индикатором на STM32.
Я набросал по-быстрому библиотеку для работы с семисегментными индикаторами для STM32, разберем, как ее конфигурировать и использовать.
Первый делом, настроим необходимую периферию, что в данном случае заключается исключительно в инициализации портов GPIO. У меня под рукой оказался следующий индикатор:
То есть именно такой, который мы среди прочих рассматривали в первой части статьи. Соответственно, необходимо 12 портов ввода-вывода, так и подключаем:
Какие именно брать порты роли никакой не играет, настраиваем просто в качестве обычных выходов:
С этим все понятно, генерируем код и добавляем в проект файлы библиотеки:
- segment_lcd.c
- segment_lcd.h
Вся связь с HAL сосредоточена только в одном месте, а именно в функции:
static void SetOutput(McuPin output, uint8_t state) { HAL_GPIO_WritePin(output.port, output.pin, (GPIO_PinState)state); }
Таким образом, максимально просто можно перейти и к варианту без HAL. Функции, которые я реализовал для использования вне драйвера:
SEG_LCD_Process();
SEG_LCD_Result SEG_LCD_WriteNumber(float number);
SEG_LCD_Result SEG_LCD_WriteString(char* str);
В целом, их назначение ясно из их же названия, тем не менее на примере посмотрим более подробно. Вторая и третья из перечисленных возвращают результат выполнения операции, и здесь два варианта:
SEG_LCD_OK
SEG_LCD_ERROR
О том, как я построил работу с экраном, я досконально рассказывать не буду. В случае возникновения вопросов, буду рад пояснить любые нюансы в комментариях или на форуме 👍
Итак, необходимая конфигурация. И, конечно же, основным моментом является задание используемых портов ввода-вывода, что осуществляется в файле segment_lcd.c:
static McuPin digitPins[DIGITS_NUM] = { {GPIOB, GPIO_PIN_3}, {GPIOB, GPIO_PIN_4}, {GPIOB, GPIO_PIN_5}, {GPIOB, GPIO_PIN_6} }; static McuPin segmentPins[SEGMENTS_NUM] = { {GPIOA, GPIO_PIN_11}, {GPIOA, GPIO_PIN_10}, {GPIOA, GPIO_PIN_9}, {GPIOA, GPIO_PIN_8}, {GPIOB, GPIO_PIN_15}, {GPIOB, GPIO_PIN_14}, {GPIOB, GPIO_PIN_13} }; static McuPin dotPin = {GPIOB, GPIO_PIN_12};
Как видите, я задал все как на схеме выше. DIGITS_NUM
определяет количество разрядов, у меня:
#define DIGITS_NUM 4
Далее еще один аспект. Помимо отображения непосредственно цифр, мне лично всегда требуется те или иные символы, в данном примере я добавил символ пробела и знак минуса. Коды, отвечающие за перевод того или иного символа или цифры в вид, потребный для вывода на семисегментный индикатор заданы здесь:
static uint8_t charactersTable[DIGIT_CHARACTERS_NUM + EXTRA_CHARACTERS_NUM] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x40, 0x00};
Каждый символ - один байт, 7 младших битов которого определяют, должен ли гореть конкретный сегмент. Первые 10 элементов массива - это цифры от 0 до 9, именно в такой последовательности, за ними же следуют дополнительные символы, список которых можно пополнить своими. Рассматриваем на примере, возьмем, третий элемент массива, то есть цифру 2, значение равно 0x5B, либо в двоичном виде 0b01011011.
Старший бит всегда нулевой, потому что сегментов 7, а битов 8, ничего неожиданного. Последующие же биты (от 6-го до 0-го) задают состояние сегментов (от g до a). Итак, цифра 2:
Должны гореть сегменты a, b, d, e, g, что и кодируется этими битами. С этим разобрались, теперь дополнительные символы. Я добавил пробел и минус, то есть два символа, это количество задано в segment_lcd.h:
#define EXTRA_CHARACTERS_NUM 2
Далее их ASCII коды:
#define ASCII_MINUS_CODE 0x2D #define ASCII_SPACE_CODE 0x20
В хэдере на этом все пока, идем обратно в segment_lcd.c и находим массив дополнительных символов:
static SEG_LCD_ExtraCharacter extraCharacters[EXTRA_CHARACTERS_NUM] = { {ASCII_MINUS_CODE, DIGIT_CHARACTERS_NUM}, {ASCII_SPACE_CODE, DIGIT_CHARACTERS_NUM + 1} };
Первое поле структуры задает ASCII код, второе - позицию символа в массиве кодов charactersTable[]
. DIGIT_CHARACTERS_NUM
- число цифр, то есть 10. Таким образом, в данном случае имеем символ минуса (ASCII_MINUS_CODE
) и его позицию: DIGIT_CHARACTERS_NUM
(10). Смотрим на 10-й элемент массива, а там у нас значение 0x40:
static uint8_t charactersTable[DIGIT_CHARACTERS_NUM + EXTRA_CHARACTERS_NUM] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x40, 0x00};
И снова иллюстрация:
Все четко, получаем символ минуса!
Упустил из виду "полярность" управления сегментами. Итак, у нас либо общий анод, либо общий катод, задается тут, для общего анода:
#define SEGMENT_PIN_ACTIVE 0
Для общего катода:
#define SEGMENT_PIN_ACTIVE 1
Ну и, пожалуй, конфигурация завершена на этом, суммарно вышло три этапа:
- задаем порты - обязательный этап
- добавляем дополнительные символы - в случае необходимости, на свой вкус, в зависимости от задачи
- задаем тип подключения 7-сегментного индикатора, общий анод или общий катод - обязательный этап
Для функционирования библиотеки достаточно добавить периодический вызов SEG_LCD_Process()
. В этой функции в том числе осуществляется переключение разрядов для динамической индикации, поэтому между вызовами следует сделать небольшую задержку:
while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ SEG_LCD_Process(); HAL_Delay(1); } /* USER CODE END 3 */
Если паузу сделать слишком большой, то соответственно, будет видно, что цифры загораются по очереди, а не горят постоянно. И финишируем небольшим демо-проектом, в котором будем менять значение переменной от -10 до 10 с шагом 0.01, производя инкрементирование каждые 10 мс. Результат прокинем на индикатор. Все манипуляции теперь только в main.c, подключаем библиотеку для семисегментных индикаторов:
/* USER CODE BEGIN Includes */ #include "segment_lcd.h" /* USER CODE END Includes */
Объявляем переменные:
/* USER CODE BEGIN PV */ float outputValue = -10; float outputStep = 0.01; float outputLimit = 10; uint32_t outputPeriod = 10; /* USER CODE END PV */
Ну и решение поставленной задачи:
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(); /* USER CODE BEGIN 2 */ uint32_t previousTime = HAL_GetTick(); uint32_t currentTime = HAL_GetTick(); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ SEG_LCD_Process(); HAL_Delay(1); currentTime = HAL_GetTick(); if ((currentTime - previousTime) >= outputPeriod) { outputValue += outputStep; if (outputValue >= outputLimit) { outputValue = (-1) * outputLimit; } SEG_LCD_WriteNumber(outputValue); previousTime = currentTime; } } /* USER CODE END 3 */ }
Вся суть в двух функциях:
SEG_LCD_Process()
SEG_LCD_WriteNumber(outputValue)
Результат работы программы таков:
И на этом заканчиваем на сегодня, библиотека писалась быстро и на коленке, параллельно с процессом написания текста статьи, так что если что пишите )
Исходный код файлов библиотеки:
/** ****************************************************************************** * @file : segment_lcd.c * @brief : 7-segment LCD driver. * @author : MicroTechnics (microtechnics.ru) ****************************************************************************** */ /* Includes ------------------------------------------------------------------*/ #include "segment_lcd.h" #include <stdio.h> /* Declarations and definitions ----------------------------------------------*/ static McuPin digitPins[DIGITS_NUM] = { {GPIOB, GPIO_PIN_3}, {GPIOB, GPIO_PIN_4}, {GPIOB, GPIO_PIN_5}, {GPIOB, GPIO_PIN_6} }; static McuPin segmentPins[SEGMENTS_NUM] = { {GPIOA, GPIO_PIN_11}, {GPIOA, GPIO_PIN_10}, {GPIOA, GPIO_PIN_9}, {GPIOA, GPIO_PIN_8}, {GPIOB, GPIO_PIN_15}, {GPIOB, GPIO_PIN_14}, {GPIOB, GPIO_PIN_13} }; static McuPin dotPin = {GPIOB, GPIO_PIN_12}; static uint8_t charactersTable[DIGIT_CHARACTERS_NUM + EXTRA_CHARACTERS_NUM] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x40, 0x00}; static SEG_LCD_ExtraCharacter extraCharacters[EXTRA_CHARACTERS_NUM] = { {ASCII_MINUS_CODE, DIGIT_CHARACTERS_NUM}, {ASCII_SPACE_CODE, DIGIT_CHARACTERS_NUM + 1} }; static uint8_t currentCharacters[DIGITS_NUM] = {0x00, 0x00, 0x00, 0x00}; static uint8_t currentDots[DIGITS_NUM] = {0, 0, 0, 0}; static uint8_t currentDigitIndex = 0; /* Functions -----------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ SEG_LCD_Result SEG_LCD_WriteString(char* str) { uint8_t currentDigitIndex = 0; for (uint8_t i = 0; i < DIGITS_NUM; i++) { currentCharacters[i] = 0x00; currentDots[i] = 0; } while(*str != '\0') { if (*str == ASCII_DOT_CODE) { if (currentDigitIndex > 0) { currentDots[currentDigitIndex - 1] = 1; } } else { if ((*str >= ASCII_NUMBER_FIRST_CODE) && (*str <= ASCII_NUMBER_LAST_CODE)) { uint8_t currentCharacterIndex = (*str - ASCII_NUMBER_FIRST_CODE); currentCharacters[currentDigitIndex] = charactersTable[currentCharacterIndex]; currentDigitIndex++; } else { uint8_t found = 0; for (uint8_t i = 0; i < EXTRA_CHARACTERS_NUM; i++) { if (*str == extraCharacters[i].asciiCode) { uint8_t currentCharacterIndex = extraCharacters[i].symbolsTableOffset; currentCharacters[currentDigitIndex] = charactersTable[currentCharacterIndex]; found = 1; currentDigitIndex++; break; } } if (found == 0) { return SEG_LCD_ERROR; } } } if (currentDigitIndex == DIGITS_NUM) { break; } str++; } if (currentDigitIndex < DIGITS_NUM) { for (int8_t i = currentDigitIndex - 1; i >= 0; i--) { currentCharacters[i + (DIGITS_NUM - currentDigitIndex)] = currentCharacters[i]; currentDots[i + (DIGITS_NUM - currentDigitIndex)] = currentDots[i]; } for (uint8_t i = 0; i < (DIGITS_NUM - currentDigitIndex); i++) { currentCharacters[i] = 0x00; currentDots[i] = 0; } } return SEG_LCD_OK; } /*----------------------------------------------------------------------------*/ SEG_LCD_Result SEG_LCD_WriteNumber(float number) { char temp[DIGITS_NUM + 2]; snprintf(temp, DIGITS_NUM + 2, "%.2f", number); SEG_LCD_WriteString(temp); return SEG_LCD_OK; } /*----------------------------------------------------------------------------*/ static void SetOutput(McuPin output, uint8_t state) { HAL_GPIO_WritePin(output.port, output.pin, (GPIO_PinState)state); } /*----------------------------------------------------------------------------*/ static void SetSegmentPins(uint8_t characterCode) { for (uint8_t i = 0; i < SEGMENTS_NUM; i++) { uint8_t bit = (characterCode >> i) & 0x01; if (bit == 1) { SetOutput(segmentPins[i], SEGMENT_PIN_ACTIVE); } else { SetOutput(segmentPins[i], !SEGMENT_PIN_ACTIVE); } } } /*----------------------------------------------------------------------------*/ void SEG_LCD_Process() { for (uint8_t i = 0; i < DIGITS_NUM; i++) { SetOutput(digitPins[i], !DIGIT_PIN_ACTIVE); } SetSegmentPins(currentCharacters[currentDigitIndex]); if (currentDots[currentDigitIndex] == 1) { SetOutput(dotPin, SEGMENT_PIN_ACTIVE); } else { SetOutput(dotPin, !SEGMENT_PIN_ACTIVE); } SetOutput(digitPins[currentDigitIndex], DIGIT_PIN_ACTIVE); currentDigitIndex++; if (currentDigitIndex == DIGITS_NUM) { currentDigitIndex = 0; } } /*----------------------------------------------------------------------------*/
/** ****************************************************************************** * @file : segment_lcd.h * @brief : 7-segment LCD driver. * @author : MicroTechnics (microtechnics.ru) ****************************************************************************** */ #ifndef SEG_LCD_H #define SEG_LCD_H /* Includes ------------------------------------------------------------------*/ #include "stm32f1xx_hal.h" /* Declarations and definitions ----------------------------------------------*/ #define DIGITS_NUM 4 #define SEGMENTS_NUM 7 #define SEGMENT_PIN_ACTIVE 0 #define DIGIT_PIN_ACTIVE !SEGMENT_PIN_ACTIVE #define DIGIT_CHARACTERS_NUM 10 #define EXTRA_CHARACTERS_NUM 2 #define ASCII_NUMBER_FIRST_CODE 0x30 #define ASCII_NUMBER_LAST_CODE 0x39 #define ASCII_MINUS_CODE 0x2D #define ASCII_SPACE_CODE 0x20 #define ASCII_DOT_CODE 0x2E typedef enum { SEG_LCD_OK, SEG_LCD_ERROR } SEG_LCD_Result; typedef struct SEG_LCD_ExtraCharacter { uint8_t asciiCode; uint8_t symbolsTableOffset; } SEG_LCD_ExtraCharacter; typedef struct McuPin { GPIO_TypeDef *port; uint16_t pin; } McuPin; /* Functions -----------------------------------------------------------------*/ extern void SEG_LCD_Process(); extern SEG_LCD_Result SEG_LCD_WriteNumber(float number); extern SEG_LCD_Result SEG_LCD_WriteString(char* str); #endif // #ifndef SEG_LCD_H
Ссылка на проект: MT_SegmentLCD_Project.