STM32即插即用HAL库驱动系列——4位串行数码管显示

使用优信电子的四位串行数码管,595驱动。

实现功能:

  1. 高级功能封装,调用函数,参数填入要显示的字符串,即可自动解析并显示。
  2. 支持左对齐、右对齐切换。
  3. 支持小数点、负数显示。
  4. 与cubemx设置完全解耦,无需参数设置,真正即插即用。

驱动部分

.h文件

cpp 复制代码
/**
  ******************************************************************************
  * @file    seg_display.h
  * @brief   74HC595驱动的4位7段数码管模块的驱动头文件
  * @author  Gemini
  * @date    2025-07-26
  * @version 3.0 (Final Clean)
  * @note    本驱动为非阻塞式,依赖一个定时器中断进行动态扫描刷新。
  * - 依赖正确的GPIO速度配置(Low Speed)来确保时序。
  * - 提供高级字符串显示接口,支持小数点和对齐。
  * - 初始化函数可自动配置定时器,具备跨平台可移植性。
  ******************************************************************************
  */

#ifndef __SEG_DISPLAY_H
#define __SEG_DISPLAY_H

#include "main.h"
#include <stdbool.h>

/* --------------------------- 公共宏定义 --------------------------- */
#define SEG_DIGITS   4    // 数码管的位数
#define SEG_BLANK    16   // 用于熄灭数码管的索引
#define SEG_DASH     17   // 用于显示'-'的索引

/* --------------------------- 公共数据类型 --------------------------- */

/**
  * @brief 对齐方式枚举
  */
typedef enum {
    SEG_ALIGN_LEFT,     // 左对齐
    SEG_ALIGN_RIGHT     // 右对齐
} SEG_Align_t;

/**
  * @brief 定义数码管一位的显示数据结构
  */
typedef struct {
    uint8_t num;        // 要显示的数字/字符索引
    bool    show_dp;    // 是否显示该位的小数点
} SEG_Digit_t;


/* --------------------------- 公共函数原型 --------------------------- */

/**
  * @brief  初始化数码管驱动并自动配置定时器
  * @param  htim: 用于驱动动态扫描的定时器句柄指针 (例如 &htim7).
  * @param  refresh_freq_hz: 每一位数码管的刷新频率 (单位: Hz). 推荐值为 400-800.
  * @retval None
  */
void SEG_Init(TIM_HandleTypeDef *htim, uint32_t refresh_freq_hz);

/**
  * @brief  高级接口:显示一个字符串(支持整数和小数)
  * @param  str: 要显示的字符串,例如 "12.34", "99.", "0.1", "5"。
  * @param  alignment: 对齐方式 (SEG_ALIGN_LEFT 或 SEG_ALIGN_RIGHT).
  * @retval None
  */
void SEG_ShowString(const char* str, SEG_Align_t alignment);

/**
  * @brief  清空所有数码管(全部熄灭)
  * @param  None
  * @retval None
  */
void SEG_Clear(void);

/**
  * @brief  数码管定时器中断服务函数 (需要在全局回调中调用)
  * @param  htim: 触发中断的定时器句柄指针.
  * @retval None
  */
void SEG_Timer_ISR(TIM_HandleTypeDef *htim);


#endif /* __SEG_DISPLAY_H */

.c文件

cpp 复制代码
/**
  ******************************************************************************
  * @file    seg_display.c
  * @brief   74HC595驱动的4位7段数码管模块的驱动源文件
  * @author  Gemini
  * @date    2025-07-26
  * @version 3.5 (Negative Number Support)
  * @note    v3.5: 增强了SEG_ShowString函数,使其能够正确解析和显示负数。
  ******************************************************************************
  */

#include "seg_display.h"
#include <string.h>

/* --------------------------- 私有静态常量 --------------------------- */
// 共阳极数码管段码表 "0123456789AbCdEF" "熄灭" "-"
static const uint8_t LED_table[18] = {
        0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E,
        0xFF, 0xBF
};
// 位选码表 (0x01对应最右边一位)
static const uint8_t wei_table[8] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};

/* --------------------------- 私有静态变量 --------------------------- */
static TIM_HandleTypeDef* p_display_timer;
static volatile SEG_Digit_t display_data[SEG_DIGITS];

/* --------------------------- 私有函数原型 --------------------------- */
static void SEG_SendByte(uint8_t byte);
static void SEG_Latch(void);
static void SEG_SetBuffer(const SEG_Digit_t* buffer);

/* --------------------------- 公共函数实现 --------------------------- */
void SEG_Init(TIM_HandleTypeDef *htim, uint32_t refresh_freq_hz)
{
    p_display_timer = htim;
    uint32_t timer_clock_frequency = HAL_RCC_GetPCLK1Freq();

    if ((RCC->CFGR & RCC_CFGR_PPRE1) != RCC_CFGR_PPRE1_DIV1)
    {
        timer_clock_frequency *= 2;
    }
    uint32_t period_value = 19;
    uint32_t prescaler_value = (timer_clock_frequency / (refresh_freq_hz * (period_value + 1))) - 1;

    htim->Instance = p_display_timer->Instance;
    htim->Init.Prescaler = prescaler_value;
    htim->Init.CounterMode = TIM_COUNTERMODE_UP;
    htim->Init.Period = period_value;
    htim->Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    if (HAL_TIM_Base_Init(htim) != HAL_OK)
    {
        Error_Handler();
    }

    HAL_GPIO_WritePin(SEG_SCLK_GPIO_Port, SEG_SCLK_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(SEG_RCLK_GPIO_Port, SEG_RCLK_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(SEG_DIO_GPIO_Port, SEG_DIO_Pin, GPIO_PIN_RESET);

    SEG_Clear();
    HAL_TIM_Base_Start_IT(p_display_timer);
}

void SEG_Clear(void)
{
    for(int i = 0; i < SEG_DIGITS; i++)
    {
        display_data[i].num = SEG_BLANK;
        display_data[i].show_dp = false;
    }
}

void SEG_ShowString(const char* str, SEG_Align_t alignment)
{
    // 1. 初始化
    SEG_Digit_t parsed_digits[SEG_DIGITS];
    int digit_count = 0;
    bool is_negative = false;
    const char* p_str = str;

    // 2. 检查负号
    if (*p_str == '-') {
        is_negative = true;
        p_str++; // 指针跳过负号
    }

    // 3. 解析数字和小数点
    for (int i = 0; p_str[i] != '\0' && digit_count < SEG_DIGITS; i++)
    {
        if (p_str[i] >= '0' && p_str[i] <= '9')
        {
            parsed_digits[digit_count].num = p_str[i] - '0';
            parsed_digits[digit_count].show_dp = (p_str[i+1] == '.');
            if (parsed_digits[digit_count].show_dp) {
                i++; // 跳过小数点
            }
            digit_count++;
        }
    }

    // 4. 创建最终显示缓冲区并清空
    SEG_Digit_t final_buffer[SEG_DIGITS];
    for(int i = 0; i < SEG_DIGITS; i++) {
        final_buffer[i].num = SEG_BLANK;
        final_buffer[i].show_dp = false;
    }

    // 5. 计算总字符数和起始物理位置
    int total_chars = digit_count + (is_negative ? 1 : 0);
    int start_pos; // 物理位置 (0=最右边)

    if (alignment == SEG_ALIGN_LEFT) {
        start_pos = SEG_DIGITS - 1;
    } else { // SEG_ALIGN_RIGHT
        start_pos = total_chars - 1;
    }

    // 6. 填充最终缓冲区
    int current_pos = start_pos;

    // 如果是负数,先放置负号
    if (is_negative) {
        if (current_pos >= 0 && current_pos < SEG_DIGITS) {
            final_buffer[current_pos].num = SEG_DASH;
            final_buffer[current_pos].show_dp = false;
        }
        current_pos--;
    }

    // 接着放置数字
    for (int i = 0; i < digit_count; i++) {
        if (current_pos >= 0 && current_pos < SEG_DIGITS) {
            final_buffer[current_pos] = parsed_digits[i];
        }
        current_pos--;
    }

    // 7. 将准备好的缓冲区内容设置到驱动
    SEG_SetBuffer(final_buffer);
}


void SEG_Timer_ISR(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == p_display_timer->Instance)
    {
        static uint8_t digit_index = 0; // 0 = 最右边一位
        uint8_t char_index = display_data[digit_index].num;
        bool    dp_state   = display_data[digit_index].show_dp;
        uint8_t segment_data = LED_table[char_index];

        if (dp_state)
        {
            segment_data &= 0x7F;
        }
        uint8_t digit_select_data = wei_table[digit_index];

        SEG_SendByte(segment_data);
        SEG_SendByte(digit_select_data);
        SEG_Latch();

        digit_index = (digit_index + 1) % SEG_DIGITS;
    }
}

/* --------------------------- 私有函数实现 --------------------------- */
static void SEG_SetBuffer(const SEG_Digit_t* buffer)
{
    // 使用 memcpy 优化缓冲区复制。
    memcpy((void*)display_data, buffer, sizeof(display_data));
}

static void SEG_SendByte(uint8_t byte)
{
    for (int i = 0; i < 8; i++)
    {
        HAL_GPIO_WritePin(SEG_DIO_GPIO_Port, SEG_DIO_Pin, (byte & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET);
        byte <<= 1;
        HAL_GPIO_WritePin(SEG_SCLK_GPIO_Port, SEG_SCLK_Pin, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(SEG_SCLK_GPIO_Port, SEG_SCLK_Pin, GPIO_PIN_SET);
    }
}

static void SEG_Latch(void)
{
    HAL_GPIO_WritePin(SEG_RCLK_GPIO_Port, SEG_RCLK_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(SEG_RCLK_GPIO_Port, SEG_RCLK_Pin, GPIO_PIN_SET);
}

使用示例:

在main.c: USER CODE BEGIN 2到USER CODE END 3部分:

cpp 复制代码
/* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM7_Init();
  /* USER CODE BEGIN 2 */

    // 运行数码管初始化

    SEG_Init(&htim7,500);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */

    while (1)

    {

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

        float tmp=-0.1f;

        for(int i=0;i<10000;++i){

            tmp+=(float)0.01;

            sprintf(display_string_buffer, "%.2f", tmp);

            SEG_ShowString(display_string_buffer,SEG_ALIGN_RIGHT);

            HAL_Delay(400);

        }

    }

  /* USER CODE END 3 */
}

在main.c: USER CODE BEGIN 4到USER CODE END 4部分:

cpp 复制代码
/* USER CODE BEGIN 4 */

/**
  * @brief  定时器周期溢出回调函数
  * @param  htim: 定时器句柄指针
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    // 判断中断是否来自于我们用于数码管刷新的TIM7
    if (htim->Instance == TIM7)
    {
        // 如果是,则调用我们驱动中写好的中断服务程序
        SEG_Timer_ISR(htim);
    }
}

/* USER CODE END 4 */

使用说明:

  • 记得在main.c里面include对应驱动的.h;
  • 同时,cubemx里面开一个tim计时器,任意计时器,任意参数均可,建议用tim7这种基础计时器,比较省资源;
  • seg_init的参数,第一个是你驱动数码管的定时器,第二个是你的目标刷新率,即数码管刷新速率,单位Hz。
相关推荐
食鹿...2 小时前
【记录贴】STM32 I2C 控制 OLED 卡死?根源在 SR1 与 SR2 的读取操作
stm32·单片机·嵌入式硬件
范纹杉想快点毕业2 小时前
《嵌入式 C 语言编码规范与工程实践个人笔记》参考华为C语言规范标准
服务器·c语言·stm32·单片机·华为·fpga开发·51单片机
Moonnnn.4 小时前
【51单片机学习】定时器、串口、LED点阵屏、DS1302实时时钟、蜂鸣器
笔记·单片机·学习·51单片机
不断提高5 小时前
多种适用于 MCU 固件的 OTA 升级方案
单片机·mcu·ota升级·双分区升级
bai5459368 小时前
STM32 DMA+AD多通道
stm32·单片机·嵌入式硬件
酷飞飞9 小时前
RTC时钟倒计时数码管同步显示实现(STC8)
单片机·嵌入式硬件·51单片机·嵌入式·stc8
✎ ﹏梦醒͜ღ҉繁华落℘11 小时前
单片机启动流程详细介绍
单片机·嵌入式硬件
2201_7562063412 小时前
day 16 stm32 IIC
stm32·单片机·嵌入式硬件
Hello_Embed14 小时前
STM32HAL 快速入门(六):GPIO 输入之按键控制 LED
笔记·stm32·单片机·嵌入式硬件·学习