STM32 四位数码管实验

预期系统功能

开发板上电后四位数码管开始计时,每1s更新一次数字,按分钟和秒显示(MM:SS),按左边按键暂停计时,按右边按键重置时间。中间两个点每0.5s闪烁一次。

系统设计

1.硬件设计

开发板使用的是意法半导体官方的STM32L053 Nucleo系列开发板,搭载STM32L053R8(低功耗STM32 MCU,64kB Flash和8kB SRAM)和ST-Link调试器,板载两个按键(用户按键和系统RESET按键)以及兼容Arduino和ST morpho的拓展接口。

LED数码管通过在PCB上的布局,形成了一个8字形的数字和一个小数点,每个可显示的笔段由一个LED对应(分别跟字母a、b、c、d、e、f、g、dp对应),这样,通过外露出的引脚来控制每个LED发光二极管的亮或灭,就可以显示出特定的数字信息。一位LED数码管的引脚和内部原理图如下所示:

引脚)内部原理图

一位数码管只能显示一位数字,如果要显示四位,按照上面的接法我们至少需要32根IO,显然是非常浪费的。而我使用的四位数码管MSQC6412C的原理图如下,4个数字分别共用阳极,控制段LED的引脚共用阴极,只需要12根IO。动态显示的原理就是“视觉暂留”原理,人眼在某个视像消失后,仍可使该物像在视网膜上滞留0.1-0.4秒左右,因此数码管用共用的IO控制亮灭,轮流显示,只要间隔很小,那么我们就会一直看到这个数码管是亮着的。

MSQC6412C数码管

按这个原理图用拓展线和面包板接线到STM32L053开发板上。红线为阳极,绿线为数字段,蓝线是DP段,PIN1~PIN12的顺序如图所示:

接线

通过查阅STM32 Nucleo-64 boards – User Manual – 6. Hardware layout and configuration,引脚对应的GPIO口等信息如下:

数码管编号 开发板物理引脚 STM32的GPIO接口
A D14 PB9
B D15 PB8
C D2 PA10
D D7 PA8
E D8 PA9
F D13 PA5
G D11 PA7
DP D12 PA6
阳极1 D3 PB3
阳极2 D4 PB5
阳极3 D5 PB4
阳极4 D6 PB10

2.软件设计

首先由于这个板子的设计原因,我找不到一组连续的接口,所以为了方便起见,我在程序里面抽象出来了一个保存GPIO接口信息的结构体,存放上表中的数据,让他们在程序上连续起来。

/* Types */
typedef struct {
    GPIO_TypeDef *x;
    uint16_t Pin;
} Connector;    //定义一个接口,不然太乱了

根据原理图可以看出数码管一次只能输出一位数组,所以如果是多位的必须使用扫描的方式输出,缩短切换的间隔,就可以达到不闪烁的效果,软件上我采用了用一个时间戳变量(time_stamp)和长度为4的数组(number[4])来显示时间,更新time_stamp时同时更新number中各个数的值以让它们正确显示,这种方式最大能显示99分59秒,因此time_stamp的最大值为5999s。而暂停的功能将由开发板上的用户按键产生中断,重置RESET按键是系统级的,无需额外编写代码即可实现触发RESET中断。
此外由于我没有找到这个MCU的库函数驱动,所以写代码的时候使用了部分HAL库以及寄存器,从实际情况来看HAL应该只是函数名字和一些变量名字不一样,其他逻辑都和库函数非常相似。main.h中的引用如下:

#include "stm32l0xx_hal.h"
#include "stm32l0xx_nucleo.h"

在HAL库中,STM官方参考文档推荐将中断抽象化,通过Callback的方式调用。在这个项目里,我们需要用到TIM2中断以及PC13上的用户按键的中断,具体实现如下:

//stm32l0xx_it.c
void TIM2_IRQHandler(void) {
  HAL_TIM_IRQHandler(&TimHandle);
}

void EXTI4_15_IRQHandler(void) {
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}

以GPIO_EXTI中断为例,在stm32l0xx_it.c中“重载”了EXTI4_15_IRQHandler之后,在其中调用HAL库中(实现在stm32l0xx_hal_gpio.c,包括下面的Callback函数也在这里)的HAL_GPIO_EXTI_IRQHandler函数,而这个函数将会自动清除中断标记位之后调用__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin),这样我们在主程序中“重载”这个函数在其中实现中断的功能就可以了。

系统实现

大部分功能都用注释说明了,以下均为main.c

/* 变量  */
Connector pos[4] = {
    {GPIOB, GPIO_PIN_3}, {GPIOB, GPIO_PIN_5}, 
    {GPIOB, GPIO_PIN_4}, {GPIOB, GPIO_PIN_10}
};  //阳极四个接口

Connector con[8] = {
    {GPIOB, GPIO_PIN_9}, {GPIOB, GPIO_PIN_8}, {GPIOA, GPIO_PIN_10},
    {GPIOA, GPIO_PIN_8}, {GPIOA, GPIO_PIN_9}, {GPIOA, GPIO_PIN_5},
    {GPIOA, GPIO_PIN_7}, {GPIOA, GPIO_PIN_6}
};  //阴极ABCDEFG + DP,输出0时点亮,输出1时熄灭

uint8_t code_table[10] = {
    0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};  //Excel生成的码表,code_table[i]对应的二进制表示数字i对应的ABCDEFG开关

static  uint16_t    time_stamp = 0; //时间秒数
static  uint8_t     dots = 0; //是否显示中间的点
static  uint8_t     number[4] = {0, 0, 0, 0};   //拆分的数字
const   uint16_t    pos_mask = GPIO_PIN_3 | GPIO_PIN_5 | GPIO_PIN_4 | GPIO_PIN_10;  //阳极四个接口的位置mask
static  int8_t      dir = 1;    //方向
TIM_HandleTypeDef TimHandle;    //stm32l0xx_it.c需要用到这个变量,所以全局了

/* 系统函数 */
static void SystemClock_Config(void);
static void Error_Handler(void);
/* 自定义函数 */
void Update_Number(int16_t);
void GPIO_Init(void);
void Clock_and_NVIC_Init(void);

int main(void)
{
    HAL_Init(); //System init
    SystemClock_Config();
    GPIO_Init();
    Clock_and_NVIC_Init();

    for (int i = 0; i < 8; i++)
    {
        //先关掉所有的LED
        HAL_GPIO_WritePin(con[i].x, con[i].Pin, GPIO_PIN_SET);
    }

    while (1)
    {
        //类似扫描,依次显示四位数字
        for (int i = 0; i < 4; i++)
        {
            //使能对应的数字并关闭其他的数字
            pos[i].x->BSRR = pos[i].Pin;
            pos[i].x->BRR = pos_mask ^ pos[i].Pin;

            //显示数字number[i]
            uint8_t t = code_table[number[i]];
            uint8_t c = 0;
            while (t > 0 || c < 8)
            {
                //本质是把码表转化为二进制,c < 8是补0用的条件
                //必须关闭不需要亮的LED,包括DP
                HAL_GPIO_WritePin(con[c].x, con[c].Pin, t % 2 ? GPIO_PIN_RESET : GPIO_PIN_SET);
                ++c;
                t /= 2;
            }
            if (dots && (i == 1 || i == 2))
            {
                //如果dots是1就打开中间两个点
                HAL_GPIO_WritePin(con[7].x, con[7].Pin, GPIO_PIN_RESET);
            }
            HAL_Delay(2); //视觉暂留
        }
    }
}

void Update_Number(int16_t value)
{
    //更新number数组的值,让它显示分钟和秒数
    if (value > 5999) value = 0;
    if (value < 0) value = 5999;
    time_stamp = value;
    number[0] = number[1] = number[2] = number[3] = 0;

    uint8_t minute = value / 60;
    uint8_t second = value % 60;

    int i = 1;
    while (minute > 0)
    {
        number[i] = minute % 10;
        --i;
        minute /= 10;
    }
    i = 3;
    while (second > 0)
    {
        number[i] = second % 10;
        --i;
        second /= 10;
    }

    return;
}

void GPIO_Init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();
    //RCC->IOPENR |= (RCC_IOPENR_GPIOAEN | RCC_IOPENR_GPIOBEN);

    GPIO_InitTypeDef a;
    a.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
    a.Mode = GPIO_MODE_OUTPUT_PP; //推挽输出模式
    a.Speed = GPIO_SPEED_FREQ_HIGH;
    a.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOA, &a);

    a.Pin = GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_10 | GPIO_PIN_8 | GPIO_PIN_9;
    HAL_GPIO_Init(GPIOB, &a);

    GPIO_InitTypeDef btn;
    btn.Pin = GPIO_PIN_13;
    btn.Pull = GPIO_PULLUP;          //上拉,无输入时是1
    btn.Mode = GPIO_MODE_IT_FALLING; //外部中断模式,下降沿
    HAL_GPIO_Init(GPIOC, &btn);
}

void Clock_and_NVIC_Init(void)
{
     //TIM2时钟使能 SET_BIT(RCC->APB1ENR, (RCC_APB1ENR_TIM2EN))
    __HAL_RCC_TIM2_CLK_ENABLE(); 
    //时钟初始化,分频到10kHz,计数周期5000即为500ms
    //SystemCoreClock为系统时钟频率(32MHz)
    TimHandle.Instance = TIM2;
    TimHandle.Init.Period = 5000 - 1;
    TimHandle.Init.Prescaler = (uint32_t)((SystemCoreClock / 10000) - 1);
    TimHandle.Init.ClockDivision = 0;
    TimHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
    HAL_TIM_Base_Init(&TimHandle);
    __HAL_TIM_CLEAR_IT(&TimHandle, TIM_IT_UPDATE);
    HAL_TIM_Base_Start_IT(&TimHandle);

    //时钟的NVIC,触发时调用TIM2_IRQHandler
    HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);

//按键的NVIC,根据13.4 EXTI interrupt/event line mapping
//PC13按钮触发的是EXTI13,调用EXTI4_15_IRQHandler
    HAL_NVIC_SetPriority(EXTI4_15_IRQn, 0, 1);
    HAL_NVIC_EnableIRQ(EXTI4_15_IRQn);
}

//GPIO按键中断事件
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)
    {
        dir = (dir == 1 ? 0 : 1);
        //把0改成-1就可以实现倒计时
    }
}

//TIM2计时中断事件
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    dots = dots ? 0 : 1;  //点点的开关
    if (dots)
        Update_Number(time_stamp + dir);
    //中间两点是半秒亮一次,一次亮半秒,所以秒数是要两次中断更新一次
}

static void SystemClock_Config这个函数我直接使用了官方例子提供的实现,有点长就不贴了,简单来说就是将时钟源配置为PLL(HSI)并将各钟时钟频率配置为如下数值,配置过程中也会实时更新SystemCoreClock的值,因此上面配置TIM的预分频器的时候可以用这个变量:

/**
  * @brief  System Clock Configuration
  *         The system Clock is configured as follow : 
  *            System Clock source            = PLL (HSI)
  *            SYSCLK(Hz)                     = 32000000
  *            HCLK(Hz)                       = 32000000
  *            AHB Prescaler                  = 1
  *            APB1 Prescaler                 = 1
  *            APB2 Prescaler                 = 1
  *            HSI Frequency(Hz)              = 16000000
  *            PLL_MUL                        = 4
  *            PLL_DIV                        = 2
  *            Flash Latency(WS)              = 1
  *            Main regulator output voltage  = Scale1 mode
  */
static void SystemClock_Config(void)

系统测试

上电后四位数码管按“分钟:秒数”的格式开始显示计时,秒数每60s进位一次。按开发板上的蓝色用户按键触发计时暂停但是中间两个点正常闪烁,按RESET重新启动计时。通过修改初始值,发现在经过5999s(即99分59秒)后正常归零重新开始,没有溢出。
观察发现数码管不同数字显示的亮度不同,初步猜测可能是没有加电阻(没有合适大小的电阻,我有的都太大了,加上会导致亮度比较低),在通电的时候,并联LED少的时候电流更大,因此“1”比“8”要亮,因为“1”只需要两个LED而“8”需要点亮全部。

本页面的全部内容在 CC BY-NC-SA 4.0 协议之条款下提供,附加条款亦可能应用
本文链接:https://blog.lonelyion.com/2020/stm32-4digital-tubes/