年初的时候,春节在家闲着无聊的东西,留个纪念。
材料
- STM8S103F3P 单片机一块
- 4位数码管一块
- PCB板/面包板以及连接线若干
我使用的是某宝上买个STM8S103F核心板,数码管为共阴数码管。仅在面包板上实验通过,尚未制成PCB板。
电路图
程序
以下代码均在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
/hide1
与show2
/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