使用优信电子的四位串行数码管,595驱动。
实现功能:
- 高级功能封装,调用函数,参数填入要显示的字符串,即可自动解析并显示。
- 支持左对齐、右对齐切换。
- 支持小数点、负数显示。
- 与cubemx设置完全解耦,无需参数设置,真正即插即用。


驱动部分
.h文件
/********************************************************************************* @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文件
/********************************************************************************* @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_RIGHTstart_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部分:
/* 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部分:
/* USER CODE BEGIN 4 *//*** @brief 定时器周期溢出回调函数* @param htim: 定时器句柄指针* @retval None*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{// 判断中断是否来自于我们用于数码管刷新的TIM7if (htim->Instance == TIM7){// 如果是,则调用我们驱动中写好的中断服务程序SEG_Timer_ISR(htim);}
}/* USER CODE END 4 */
使用说明:
- 记得在main.c里面include对应驱动的.h;
- 同时,cubemx里面开一个tim计时器,任意计时器,任意参数均可,建议用tim7这种基础计时器,比较省资源;
- seg_init的参数,第一个是你驱动数码管的定时器,第二个是你的目标刷新率,即数码管刷新速率,单位Hz。