STM8S制作数字时钟

年初的时候,春节在家闲着无聊的东西,留个纪念。

材料

  • STM8S103F3P 单片机一块
  • 4位数码管一块
  • PCB板/面包板以及连接线若干

我使用的是某宝上买个STM8S103F核心板,数码管为共阴数码管。仅在面包板上实验通过,尚未制成PCB板。

电路图

STM8S制作数字时钟_第1张图片
数字时钟电路图

程序

以下代码均在IAR下测试通过,由于我并不会使用C,因此代码可能比较丑陋,见谅。

控制数码管

对于控制数码管显示数字,我大量使用了硬编码,因此代码很难看。头文件如下:(若实际使用中与上面的线路图连接不一致,只需要修改此头文件的相关定义即可。)

/*
 * tube.h
 *
 *  Created on: 2016年2月3日
 *      Author: forDream
 */

#ifndef PROJECT_STM8S_STDPERIPH_TEMPLATE_DRIVE_TUBE_H_
#define PROJECT_STM8S_STDPERIPH_TEMPLATE_DRIVE_TUBE_H_
#define GPIO_MODE GPIO_MODE_OUT_PP_LOW_FAST
// 分组定义
#define LED01_GROUP (GPIOA) // 数码管1号针脚 对应分组
#define LED02_GROUP (GPIOA) // 数码管2号针脚 对应分组
#define LED03_GROUP (GPIOD) // 数码管3号针脚 对应分组
#define LED04_GROUP (GPIOB) // 数码管4号针脚 对应分组
#define LED05_GROUP (GPIOB) // 数码管5号针脚 对应分组
#define LED06_GROUP (GPIOA) // 数码管6号针脚 对应分组

#define LED07_GROUP (GPIOD) // 数码管7号针脚 对应分组
#define LED08_GROUP (GPIOC) // 数码管8号针脚 对应分组
#define LED09_GROUP (GPIOC) // 数码管9号针脚 对应分组
#define LED10_GROUP (GPIOC) // 数码管10号针脚 对应分组
#define LED11_GROUP (GPIOC) // 数码管11号针脚 对应分组
#define LED12_GROUP (GPIOC) // 数码管12号针脚 对应分组

// 第一组 针脚定义
#define LED01 GPIO_PIN_2 // 数码管 01 针脚
#define LED02 GPIO_PIN_1 //
#define LED03 GPIO_PIN_5
#define LED04 GPIO_PIN_5
#define LED05 GPIO_PIN_4
#define LED06 GPIO_PIN_3

// 第二组
#define LED07 GPIO_PIN_2 // 数码管 07 针脚 分组B
#define LED08 GPIO_PIN_7
#define LED09 GPIO_PIN_6
#define LED10 GPIO_PIN_5
#define LED11 GPIO_PIN_4
#define LED12 GPIO_PIN_3

void initTube();
void showNum(int digital, int number,int dp);
void showDP(int isShow);
void reverseDP();
void testTube();
#endif /* PROJECT_STM8S_STDPERIPH_TEMPLATE_DRIVE_TUBE_H_ */

此处为共阴数码管,若为共阳,则倒置show1/hide1show2/hide2函数即可。

/*
 * tube.c
 *
 *  Created on: 2016年2月3日
 *      Author: forDream
 */
#include "tube.h"
#include "stm8s.h"

/**
 * 显示具体笔画
 */
void show1(GPIO_TypeDef * port, unsigned char pin) {
    GPIO_WriteLow(port, pin);
}
/**
 * 隐藏具体笔画
 */
void hide1(GPIO_TypeDef * port, unsigned char pin) {
    GPIO_WriteHigh(port, pin);
}
/**
 * 显示数码管对应位数
 */
void show2(GPIO_TypeDef * port, unsigned char pin) {
    GPIO_WriteHigh(port, pin);
}
/**
 * 隐藏数码管对应位数
 */
void hide2(GPIO_TypeDef * port, unsigned char pin) {
    GPIO_WriteLow(port, pin);
}
void showA(int isShow) {
    if (isShow)
        show1(LED11_GROUP, LED11);
    else
        hide1(LED11_GROUP, LED11);
}

void showB(int isShow) {
    if (isShow)
        show1(LED07_GROUP, LED07);
    else
        hide1(LED07_GROUP, LED07);
}
void showC(int isShow) {
    if (isShow)
        show1(LED04_GROUP, LED04);
    else
        hide1(LED04_GROUP, LED04);
}
void showD(int isShow) {
    if (isShow)
        show1(LED02_GROUP, LED02);
    else
        hide1(LED02_GROUP, LED02);
}
void showE(int isShow) {
    if (isShow)
        show1(LED01_GROUP, LED01);
    else
        hide1(LED01_GROUP, LED01);
}
void showF(int isShow) {
    if (isShow)
        show1(LED10_GROUP, LED10);
    else
        hide1(LED10_GROUP, LED10);
}
void showG(int isShow) {
    if (isShow)
        show1(LED05_GROUP, LED05);
    else
        hide1(LED05_GROUP, LED05);
}
void showDP(int isShow) {
    if (isShow)
        show1(LED03_GROUP, LED03);
    else
        hide1(LED03_GROUP, LED03);
}
void reverseDP(){
        GPIO_WriteReverse(LED03_GROUP, LED03);
}

volatile static int tag=0;

void initTube(){
    if (tag == 0) {
        tag = -1;
        /*
        GPIO_DeInit(LED01_GROUP);
        GPIO_DeInit(LED02_GROUP);
        GPIO_DeInit(LED03_GROUP);
        GPIO_DeInit(LED04_GROUP);
        GPIO_DeInit(LED05_GROUP);
        GPIO_DeInit(LED06_GROUP);
        GPIO_DeInit(LED07_GROUP);
        GPIO_DeInit(LED08_GROUP);
        GPIO_DeInit(LED09_GROUP);
        GPIO_DeInit(LED10_GROUP);
        GPIO_DeInit(LED11_GROUP);
        GPIO_DeInit(LED12_GROUP);
        */

        GPIO_Init(LED01_GROUP, LED01, GPIO_MODE);
        GPIO_Init(LED02_GROUP, LED02, GPIO_MODE);
        GPIO_Init(LED03_GROUP, LED03, GPIO_MODE);
        GPIO_Init(LED04_GROUP, LED04, GPIO_MODE);
        GPIO_Init(LED05_GROUP, LED05, GPIO_MODE);
        GPIO_Init(LED06_GROUP, LED06, GPIO_MODE);

        GPIO_Init(LED07_GROUP, LED07, GPIO_MODE);
        GPIO_Init(LED08_GROUP, LED08, GPIO_MODE);
        GPIO_Init(LED09_GROUP, LED09, GPIO_MODE);
        GPIO_Init(LED10_GROUP, LED10, GPIO_MODE);
        GPIO_Init(LED11_GROUP, LED11, GPIO_MODE);
        GPIO_Init(LED12_GROUP, LED12, GPIO_MODE);
    }
}
void showNum(int digital, int number,int dp) {
    unsigned char digitalMap[] = { 0, 12, 9, 8, 6 };
    // 关闭所有位
    hide2(LED12_GROUP, LED12);
    hide2(LED09_GROUP, LED09);
    hide2(LED08_GROUP, LED08);
    hide2(LED06_GROUP, LED06);
    // 关闭所有笔画
    showA(0);
    showB(0);
    showC(0);
    showD(0);
    showE(0);
    showF(0);
    showG(0);
        showDP(dp);
    // 显示笔画
    switch (number) {
        case 0:
        showA(1);
        showB(1);
                showC(1);
        showD(1);
        showE(1);
        showF(1);
        break;
    case 1:
        showB(1);
        showC(1);
        break;
    case 2:
        showA(1);
        showB(1);
        showG(1);
        showE(1);
                showD(1);
        break;
    case 3:
        showA(1);
        showB(1);
        showG(1);
        showC(1);
        showD(1);
        break;
    case 4:
        showF(1);
        showG(1);
        showB(1);
        showC(1);
        break;
    case 5:
        showA(1);
        showF(1);
        showG(1);
        showC(1);
        showD(1);
        break;
    case 6:
        showA(1);
        showF(1);
        showE(1);
        showD(1);
        showC(1);
        showG(1);
        break;
    case 7:
        showA(1);
        showB(1);
        showC(1);
        break;
    case 8:                
        showA(1);
        showB(1);
                showC(1);
        showD(1);
        showE(1);
        showF(1);
        showG(1);
        break;
    case 9:
        showA(1);
        showB(1);
        showC(1);
        showD(1);
        showF(1);
        showG(1);
        break;
    }
    // 打开对应位数
    // 关闭所有位
    switch (digital) {
    case 1:
        show2(LED12_GROUP, LED12);
        break;
    case 2:
        show2(LED09_GROUP, LED09);
        break;
    case 3:
        show2(LED08_GROUP, LED08);
        break;
    case 4:
        show2(LED06_GROUP, LED06);
        break;
    }
}

void testTube(){
    showA(1);
    showB(1);
    showC(1);
    showD(1);
    showE(1);
    showF(1);
    showG(1);
    showDP(1);
    show2(LED12_GROUP, LED12);
    show2(LED09_GROUP, LED09);
    show2(LED08_GROUP, LED08);
    show2(LED06_GROUP, LED06);
}

计算时间的流逝

由于单片机无法得知真实时间的流逝,因此需要通过单片机的时钟频率计算得出对应的真实世界的时间。STM8S103默认启动使用内部高速时钟,8分频,即16MHz / 8 = 2MHz。那么一个时钟周期就对应真实世界的1 / 2MHz = 0.5 us,这个结果在后面的延时函数中有用到。我在这里使用了定时器,来计算时间的流逝,因为在main方法中,我可能会随时修改循环体的代码,所以我无法准确计算出一次循环的准确时间,此时采用定时器的中断,能够较为准确的获得时间的流逝。由于默认启动为2MHz的时钟频率,因此我设置的定时器采用1000分频,自动重载2000,即1 / (2MHz / 1000) *2000 = (1 / (1 / 500)) us * 2000 = 500us *2000 = 1000000us = 1000ms = 1s,正好1秒触发一次中断。

注:可能是我淘宝上买的便宜货原因,芯片的内部时钟其实误差有点大,这样计算出来的理论时间与实际间隔比较容易有出入。如果需要比较准确的计时,可以考虑使用外部晶振。

主程序

代码注释比较详细,不再重复说明。

main.h

#ifndef PROJECT_STM8S_STDPERIPH_TEMPLATE_MAIN_H_
#define PROJECT_STM8S_STDPERIPH_TEMPLATE_MAIN_H_
typedef enum {NORMAL, SETTLE} AppMode;
typedef enum {HOURS,MINITES} PartOfSettle;
void add1Sec();
void toggleDP();
void delay_nms(int ms);
void changeMode(AppMode mode);
AppMode currentMode();
void toggleSettle();
#endif /* PROJECT_STM8S_STDPERIPH_TEMPLATE_MAIN_H_ */

main.c

/* Includes ------------------------------------------------------------------*/
#include "stm8s.h"
#include "stm8s_it.h"
#include "tube.h"
#include "main.h"
/* Private defines -----------------------------------------------------------*/
#define PRESSED RESET // 按键按下

volatile AppMode appMode = NORMAL;
volatile PartOfSettle settle = MINITES;
volatile int hh,mm,ss;
volatile int dpFlag=1;

void delay_1ms(){
  for(int i=0;i<400;i++) asm("nop");
}
// 延迟n毫秒
void delay_nms(int ms) {
  for(int i=0;i 23) hh = 0;
  }else{
    if(++mm > 59) mm = 0;
  }
}
/* Private function prototypes -----------------------------------------------*/
/* Private functions ---------------------------------------------------------*/
// 初始化定时器,用于计算时间
void initTim(){
  TIM1_TimeBaseInit(999, TIM1_COUNTERMODE_UP, 2000, 0); // 默认启动16MHz,8分频,即2MHz。定时器1秒一次中断
  TIM1_ITConfig(TIM1_IT_UPDATE, ENABLE);
  TIM1_ARRPreloadConfig(ENABLE);
  TIM1_SetCounter(0x0000);
  TIM1_Cmd(ENABLE);
  enableInterrupts();
}
/**
* 计数时间+1秒
*/
void add1Sec(){
  dpFlag ^= 1; // 一秒钟,数码管中时间的分割点跳动一次
  if(++ss>59){
    ss = 0;
    if(++mm>59){
      mm = 0;
      if(++hh>23)
        hh = 0;
    }
  }
}
// 初始化按键状态 -- 置为上拉输出无中断模式
void initButton(){
  GPIO_Init(GPIOD, GPIO_PIN_6, GPIO_MODE_IN_PU_NO_IT);
  GPIO_Init(GPIOD, GPIO_PIN_4, GPIO_MODE_IN_PU_NO_IT);
}
// 获得设置按键的状态 - 进入/退出设置模式
BitStatus readButtonSet(){
  return GPIO_ReadInputPin(GPIOD, GPIO_PIN_4);
}
// 获得设值按键的状态 - 调整时钟数字
BitStatus readButtonValue(){
  return GPIO_ReadInputPin(GPIOD, GPIO_PIN_6);
}
void main(void)
{
    initButton();
    hh=00;
    mm=00;
    ss=00;
    initTube();
    initTim();
    int a,b,c,d;
    int press_tick = 0;
    while(1){
      // 判定 设置键 状态
      if(readButtonSet() == PRESSED){
        if(appMode == NORMAL){ // 从正常模式 切换到设置模式
          if(++press_tick > 1000){
            // 设置模式
            appMode = SETTLE;
            press_tick = 0;
            delay_nms(2000); // 延迟2s 给用户提示,防止按键时间过长,进入设置模式后,再次退出,延时状态下,数码管会停止显示时间
          }  
        }else if(appMode == SETTLE){ // 已经在设置模式,则切换时分秒的修改或退出设置模式
          if(++press_tick > 1000){ // 短按30次 会自动认为是长按
            appMode = NORMAL;
            press_tick = 0;
            delay_nms(2000); // 延迟2s,给用户提示,防止按键时间过长,退出设置模式后,再次进入
          }else{ //若不为长按,则切换设置的高低位
            toggleSettle();
          }
        }
      }else if(readButtonValue() == PRESSED){ // 是否为 调整键 按下
        if(appMode == SETTLE){
          if(--press_tick <= 0){ // 控制跳转速率 防止长按状态下,跳动过快
            valueUp();
            press_tick = 500;
          }
        }
      }else{
        press_tick = 0; // 清除计数,防止多次短按后误认为长按
      }
      a = hh / 10;
      b = hh % 10;
      c = mm / 10;
      d = mm % 10;
      // 判定当前程序模式
      if(appMode == NORMAL){        
        showNum(1,a,dpFlag);
        //delay_nms(7);
        showNum(2,b,dpFlag);
        //delay_nms(7);
        showNum(3,c,dpFlag); 
        //delay_nms(7);
        showNum(4,d,dpFlag);
        //delay_nms(7);
      }else if(appMode == SETTLE){
        // 设置模式下,dpFlag 临时充当 当前设置位的标志,用于闪烁显示
        // 当前设置的数位 进行闪烁显示,即 设置小时数时,小时数闪烁,否则为分钟数闪烁
        if(settle == HOURS){
          if(dpFlag){
            showNum(1,a,1);
            showNum(2,b,1);
          }
          showNum(3,c,1);
          showNum(4,d,1);
        }else if(settle == MINITES){
          showNum(1,a,1);
          showNum(2,b,1);
          if(dpFlag){
            showNum(3,c,1);
            showNum(4,d,1);
          }
        }
      }
    }
}

#ifdef USE_FULL_ASSERT

/**
  * @brief  Reports the name of the source file and the source line number
  *   where the assert_param error has occurred.
  * @param file: pointer to the source file name
  * @param line: assert_param error line source number
  * @retval : None
  */
void assert_failed(u8* file, u32 line)
{ 
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */

  /* Infinite loop */
  while (1)
  {
  }
}
#endif

定时器中断

在stm8s_it.c中找到INTERRUPT_HANDLER(TIM1_UPD_OVF_TRG_BRK_IRQHandler, 11)的方法签名,修改如下:

INTERRUPT_HANDLER(TIM1_UPD_OVF_TRG_BRK_IRQHandler, 11)
{
  /* In order to detect unexpected events during development,
     it is recommended to set a breakpoint on the following instruction.
  */
  add1Sec(); // 增加一秒时间
  TIM1_ClearFlag(TIM1_FLAG_UPDATE); // 清除中断标志位
}

测试

此时按线路图连接,启动程序。应该就已经完成了,在正常模式下,长按『设置键』进入设置模式(按『设值键』无任何效果),在设置模式下,正在被设置位数(小时数或分钟数)会闪烁显示,此时可以按『设值键』对该数字进行加一操作,也可以长按『设值键』进行连续的加一操作。同时可以短按『设置键』切换设置小时数或分钟数。设置完成后,长按『设置键』退出设置模式,返回正常模式。

需要注意的是,我使用的数码管为没有小数点的数码管,在第二位与第三位数之间是竖着的两个点,作为时钟分隔符(分别连接着第三位与第四位的共阴极)。每位数右下角的四个小数点均不亮,如果你使用的数码管为普通数码管,非显示时间的数码管,在main.c的循环中,需要对小数点的显示做部分调整。

这个应用我做了将近半个月,其中有关于时间的计算,对我造成了极大的困扰,一直不理解如何比较准确的计算真实世界的时间流逝。对于软延迟而言,假设MCU时钟频率为X MHz,即一个时钟周期需要(1/X)us,因此可以通过计算指令的时钟周期,获得准确的时间。对于定时器而言,假设MCU时钟频率为X MHz,分频P,自动重载Y,重复计数Z,则一次中断的时间为((1/(X / P))YZ)us,重复计数器好像只有TIM1支持,其他计数器的不支持则重复计数器为1。

源代码

iar的源代码一份,基于STM8S官方例程修改。源码中使用的引脚与之前电路图中绘制的有所不同。STM8S_StdPeriph_Template

你可能感兴趣的:(STM8S制作数字时钟)