找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 50|回复: 3
打印 上一主题 下一主题
收起左侧

STC8单片机超简单TM1650数码管驱动库函数

[复制链接]
回帖奖励 1 黑币 回复本帖可获得 1 黑币奖励! 每人限 1 次(中奖概率 70%)
跳转到指定楼层
楼主
ID:1155837 发表于 2026-2-3 14:17 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
从51单片机转到32单片机之后,我偶然间因为一个ESP32的开源项目,接触了Arduino,然后我就被Arduino简洁的库函数惊讶到了,因此我就想着,我也可以写一个,借此来深入学习库函数的搭建。
正好,最近做的项目需要用数码管显示电压,而我打算使用的STC8单片机也是有硬件I2C的,不用软件跑时序。那么说写就写!
什么是Arduino风格呢?就是将复杂的函数封装起来,最后呈现出来,只需要给一个简单的函数名填入一些参数,就可以调用复杂的库函数,实现功能。对于TM1650来说,可以是这样的:
void TM1650_Init(unsigned int TM1650_Freq_kHz,unsigned char Brt)
通过传入Freq和Brt这两个变量,就可以轻松的配置I2C频率,设置TM1650的亮度。比如TM1650_Init(100,4),就可以将I2C频率设置为100khz,将亮度设置为4级。函数内部会自动根据MainFOSC计算寄存器值,并赋予。
又比如这样
void TM1650_PinSet(unsigned char SCL,unsigned char SDA)

通过这个函数,可以简单的配置I2C引脚,比如TM1650_Pinset(25,24),可以将I2C引脚设置到P25和P24。当然,不同型号可能不一样,特别是8脚芯片完全不一样。而且STC8没有GPIO Matrix(GPIO交换矩阵),无法将功能脚任意映射,所以这个函数比较局限。
再比如
TM1650_Display_Num(unsigned int Num,unsigned char Dot_pos)

通过这个函数,就可以控制显示的数值和小数点位置,比如(1234,2),就会显示12.34
或者是这样
TM1650_Display_Word(const char *Word)
这个函数需要传入一个字符串,比如("STOP"),函数内部就会拆分字符串,并根据ASCII码作为索引,在数码管上显示字母。不过由于七段数码管的局限性,能显示的东西还是不多。不过由于函数内部特性,你甚至可以传入("S.T.O.P.")这种全是小数点的,很自由。
这就是Arduino风格
怎么样,是不是很直观?
我将本库函数头文件比较长,两百多行,因此我将包含全部库函数的头文件TM1650.H作为附件上传,需要的可以下载,至于怎么在KEIL中引用?可以通过keil导入“存在的文档”即可。
而每个函数详细的说明,会在之后的楼层一个个更新.

TM1650.zip

3.57 KB, 下载次数: 0, 下载积分: 黑币 -5

评分

参与人数 1黑币 +90 收起 理由
admin + 90 共享资料的奖励!

查看全部评分

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏1 分享淘帖 顶 踩
回复

使用道具 举报

沙发
ID:1155837 发表于 2026-2-3 14:53 | 只看该作者
本代码完全依靠硬件I2C实现,因此大概只能在STC8单片机上使用。并且需要使用1.8kb flash,因为没有用浮点,string还有sprintf等函数,因此本库函数的flash占用算是很小了。
具体占用Program Size: data=9.1 xdata=38 const=9 code=1848
TM1650作为一个类I2C总线器件,可以将其“数据命令”0x48作为I2C地址来使用,效果是一样的。
要使用本库函数,一定要在主函数中声明unsigned long Main_FOSC = 24000000L;
字母需要保持一致,否则可能会导致报错或者库函数无法正确计算I2C时钟频率,或者你可以手动修改头文件。
其他I2C也就不需要了
对了。一般来说TM1650的亮度设置到4级就够用了,否则发热过大。
下面的函数包括硬件I2C寄存器值初始化,以及总线时钟频率计算并赋予。根据手册中公式计算,已经验证通过。
void TM1650_Init(unsigned int TM1650_Freq_kHz,unsigned char Brt){
    unsigned char MSSPEED = 0;
    P_SW2 |= 0X80;//允许访问扩展寄存器
    if(TM1650_Freq_kHz != 0){//传入0代表不重新初始化I2C
    I2CCFG = 0XE0;//启用I2C,主机模式
    MSSPEED = (((Main_FOSC/4)/(TM1650_Freq_kHz * 1000UL)) - 2) & 0X3F;//与运算只保留低六位
    I2CCFG = (I2CCFG & 0XC0) | MSSPEED;//写入低六位
    I2CMSCR = 0X00;//关闭I2C主机模式中断,清空I2C命令
    I2CTXD = 0X00;//清空发送数据寄存器
    I2CRXD = 0X00;//清空接收数据寄存器
    I2CMSAUX = 0X00;//关闭主机自动发送功能
    I2CMSST = 0X00;//清零标志
    }
    TM1650_Write(TM1650_CMD2[Brt],TM1650_CMD1);//发送命令1(0X48),再发送命令2(亮度)
    //MSBUSY指示I2C控制器是否忙碌,但本驱动程序不需要检查MSBUSY
}
回复

使用道具 举报

板凳
ID:1155837 发表于 2026-2-3 14:53 | 只看该作者
其实这次写的有不少巧妙的地方,比如下面这个函数。
在C51下,一个float变量就需要给程序体积增加2kb,对于寸土寸金的8kb flash的小单片机,是无法接受的,因此只能优化。
因此,在这里,我使用的是传入整数+小数点的方法。我也没有用sprintf这个格式化输出函数,而是直接根据四位数值的特性,写了一套根据ASCII码特性的数值转字符串方法。
另外,在整个库函数里,查表,按段写显存都是基于ASCII码和字符串的特性实现。因为每个字符都对应了一个0~127的7位ASCII码。而一个字符串,则是由ASCII码构成的,结尾为\0的数组,因此我们可以用指针变量来指向这个数组,并读取这个字符串数组中的数值,并通过\0来检测结束。
void TM1650_Display_Num(unsigned int Num, unsigned char Dot_pos){
    //整数转字符所用变量
    char Trans_Temp[4];//整数截取低四位的缓存
    char TM1650_String_Buffer[8];//最终输出的字符串缓存区
    char *Ptr = TM1650_String_Buffer;//数组的指针,用于在数组取值
    unsigned char i = 4; //数据保留低四位
    unsigned char j = 0;
    unsigned char k = 0;
    //写入TM1650所用变量
    bit TM1650_Need_DP = 0;//是否需要小数点标志
    unsigned char TM1650_Current_Digit = 0;//记录当前写的数码管段
    unsigned char TM1650_Write_Buffer = 0;//写过程的缓存区域
    //整数转字符
    //先把整数拆解成纯数字数组 (倒序存放)
    //比如 1234 -> temp[0]=4, temp[1]=3, temp[2]=2, temp[3]=1
    for(k=0; k<4; k++) {
        Trans_Temp[k] = (Num % 10) + '0';
        Num /= 10;
    }
    //根据 dot_pos 组装带点的字符串
    //从高位往低位填入buffer
    for(k=0; k<4; k++) {
        TM1650_String_Buffer[j++] = Trans_Temp[3-k]; //填入数字
        if(Dot_pos == (k + 1) && k < 3) { //如果到了设定的点位
            TM1650_String_Buffer[j++] = '.';
        }
    }
    TM1650_String_Buffer[j] = '\0'; //封口
    //字符数组读取与写入显存
    while(*Ptr != '\0' && TM1650_Current_Digit < 4){//字符串未结尾,且并没有写完完整4位
        if(*(Ptr+1) == '.'){//检测下一位是否是小数点(TM1650的小数点与上一个段绑定)
            TM1650_Need_DP = 1;//标记需要小数点
        }
        TM1650_Write_Buffer = Seg_Table(*Ptr);//按字符ASCII值查表
        if(TM1650_Need_DP){//如果需要小数点
            TM1650_Write_Buffer |= 0x80;//最高位置1
            TM1650_Need_DP = 0;//清零标志
            Ptr = Ptr + 1;//下一位存在小数点,直接跳过
            }
        switch(TM1650_Current_Digit){//写入
            case 0: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG1); break;
            case 1: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG2); break;
            case 2: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG3); break;
            case 3: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG4); break;
            }
            TM1650_Current_Digit++;//数码管段+1
            Ptr++;//指针+1
        }
}
回复

使用道具 举报

地板
ID:1155837 发表于 2026-2-3 15:00 | 只看该作者
下面是全部代码,可以自己手动创建头文件,复制进去即可。
//本头文件用于TM1650四位数码管芯片
//时间2026年1月15日20:45:48
//本头文件不使用I2C中断,未开启,如需要,请手动开启。
//Written by SY_studio 2026/02/03
#ifndef __TM1650_H__
#define __TM1650_H__
#include "STC8H.H"
//显存地址
#define TM1650_CMD1 0x48//TM1650数据命令,
#define TM1650_DIG1 0x68//四段分别地址
#define TM1650_DIG2 0x6A//一般DIG1为最高显示位
#define TM1650_DIG3 0x6C
#define TM1650_DIG4 0x6E
//取模表
#define SEG_0 0X3F
#define SEG_1 0X06
#define SEG_2 0X5B
#define SEG_3 0X4F
#define SEG_4 0X66
#define SEG_5 0X6D
#define SEG_6 0X7D
#define SEG_7 0X07
#define SEG_8 0X7F
#define SEG_9 0X6F
#define SEG_Blank 0X00
#define SEG_Minus 0X40
#define SEG_E 0X79
#define SEG_r 0X50
#define SEG_O 0X3F
#define SEG_F 0X71
#define SEG_n 0X54
#define SEG_U 0X3E
#define SEG_V 0X3E
#define SEG_P 0X73
#define SEG_C 0X39
#define SEG_A 0X77
#define SEG_B 0X7F
#define SEG_b 0X7C
#define SEG_T 0X46
#define SEG_S 0X6D
#define SEG_L 0X38
#define SEG_H 0X76
#define SEG_I 0X06
#define SEG_o 0X5C
//I2C命令表
#define I2C_IDLE 0X00
#define I2C_START 0X01
#define I2C_STOP 0X06
#define I2C_SENDMS 0X02
#define I2C_RECV_ACK 0X03
//#include "TM1650_SEGMAP.H"
extern unsigned long Main_FOSC;//使用主程序中定义的时钟频率
const unsigned char TM1650_CMD2[] = {//此表决定了发送的亮度数值
    0x00,// 0 关闭显示
    0x11,// 1级亮度
    0x21,// 2级亮度
    0x31,// 3级亮度
    0x41,// 4级亮度
    0x51,// 5级亮度
    0x61,// 6级亮度
    0x71,// 7级亮度
    0x01 // 8级亮度
};
//frequency单位为k,brightness取0时关闭数码管输出
//num为要显示的数字,取值0到9999,DP为小数点位置,取值0到3
unsigned char Seg_Table(unsigned char word) {
    switch (word) {
        case '0': return SEG_0;      // 0x3F
        case '1': return SEG_1;      // 0x06
        case '2': return SEG_2;      // 0x5B
        case '3': return SEG_3;      // 0x4F
        case '4': return SEG_4;      // 0x66
        case '5': return SEG_5;      // 0x6D
        case '6': return SEG_6;      // 0x7D
        case '7': return SEG_7;      // 0x07
        case '8': return SEG_8;      // 0x7F
        case '9': return SEG_9;      // 0x6F
        case 'A': return SEG_A;      // 0x77
        case 'B': return SEG_B;      // 0x7F
        case 'C': return SEG_C;      // 0x39
        case 'E': return SEG_E;      // 0x79
        case 'F': return SEG_F;      // 0x71
        case 'H': return SEG_H;      // 0x76
        case 'L': return SEG_L;      // 0x38
        case 'P': return SEG_P;      // 0x73
        case 'S': return SEG_S;      // 0x6D
        case 'T': return SEG_7;      // 0x07
        case 'U': return SEG_U;      // 0x3E
        case 'V': return SEG_V;      // 0x3E
        case 'O': return SEG_O;
        //小写字母
        case 'b': return SEG_b;      // 0x7C
        case 'n': return SEG_n;      // 0x54
        case 'r': return SEG_r;      // 0x44
        case 'o': return SEG_o;      // 0x3F
        case 'I': return SEG_I;      // 0x06
        //标点
        case ' ': return SEG_Blank;  // 空格显示空白
        case '-': return SEG_Minus;  // 0x40
        default: return SEG_Blank;  // 未定义字符显示空白
    }
}
void I2C_CMD(unsigned char CMD){//I2C命令设置
    I2CMSCR = (I2CMSCR & 0XF0) | CMD;//只填低四位
    //头文件中没有MSIF的定义,因此只能手动做与运算
    while(!(I2CMSST & 0X40));//等待I2C控制器执行完命令
    I2CMSCR = (I2CMSCR & 0XF0) | I2C_IDLE;//清空I2C命令
    I2CMSST &= ~0X40;//清除完成标志(~0X40 = 10111111,做与运算,可清零bit6位)
}
bit I2C_Check_ACK(void){//TM1650不需要确认ACK
    unsigned char a = 0;
    a = (I2CMSST & 0X02);//读取I2CMSST B1位是否为1
    return a;//返回1或0
}
void TM1650_Write(unsigned char Data,unsigned char Addr){//底层发送数据驱动
    //此处不需I2C开始与停止发送,而是手动添加,可连续发送。
    if(Addr != 0){//如果需要在同一地址发送超过8bit数据,可令Addr=0
        I2C_CMD(I2C_START);
        I2CTXD = Addr;//不需要担心时序,每字节后必须有一个ack,时间足够
        I2C_CMD(I2C_SENDMS);//发送命令
        I2C_CMD(I2C_RECV_ACK);//接受ACK
        I2CTXD = Data;//先发地址,后发数据
        I2C_CMD(I2C_SENDMS);
        I2C_CMD(I2C_RECV_ACK);
        I2CTXD = 0;//清空发送寄存器
        I2C_CMD(I2C_STOP);
    }
    else{//往一个八位地址一直发数据,比如写OLED屏(特殊设备有16位I2C地址,在这里不做讨论)
        I2CTXD = Data;//先发地址,后发数据
        I2C_CMD(I2C_SENDMS);
        I2C_CMD(I2C_RECV_ACK);
    }
}
void TM1650_PinSet(unsigned char SCL,unsigned char SDA){
    P_SW2 = 0X80;
    //仅对STC8H和G系列部分有效
    //本函数请谨慎使用,务必先查看技术手册中的引脚定义,或者手动规定引脚
    //对于所有非法值,都不会改变I2C引脚
    if(SCL == 15 && SDA == 14){
        P_SW2 &= ~0X30;//B5,B4置0
        P1M0 |= 0x30; P1M1 |= 0x30; //P15 P14设置为开漏
    }
    else if(SCL == 25 && SDA == 24){
        P_SW2 &= ~0X30;//B5,B4置0
        P_SW2 |= 0X10;//B4置1
        P2M0 = 0X30;P2M1 = 0X30;//P25 P24开漏
    }
    else if(SCL == 77 && SDA == 76){
        P_SW2 &= ~0X30;//B5,B4置0
        P_SW2 |= 0X20;//B5置1
        P7M0 |= 0xC0;P7M1 |= 0xC0;//P77 P76开漏
    }
    else if(SCL == 32 && SDA == 33){
        P_SW2 &= ~0X30;//B5,B4置0
        P_SW2 |= 0X30;//B5,B4置1
        P3M0 = 0x0C;P3M1 = 0X0C;//P32,P33开漏
    }
    else if(SCL == 54 && SDA == 55){
        P_SW2 &= ~0X30;//B5,B4置0
        P_SW2 |= 0X10;//仅对STC8G1K08A 8PIN有效
        P5M0 = 0X30;P5M1 = 0X30;
    }
}
void TM1650_Init(unsigned int TM1650_Freq_kHz,unsigned char Brt){
    unsigned char MSSPEED = 0;
    P_SW2 |= 0X80;//允许访问扩展寄存器
    if(TM1650_Freq_kHz != 0){//传入0代表不重新初始化I2C
    I2CCFG = 0XE0;//启用I2C,主机模式
    MSSPEED = (((Main_FOSC/4)/(TM1650_Freq_kHz * 1000UL)) - 2) & 0X3F;//与运算只保留低六位
    I2CCFG = (I2CCFG & 0XC0) | MSSPEED;//写入低六位
    I2CMSCR = 0X00;//关闭I2C主机模式中断,清空I2C命令
    I2CTXD = 0X00;//清空发送数据寄存器
    I2CRXD = 0X00;//清空接收数据寄存器
    I2CMSAUX = 0X00;//关闭主机自动发送功能
    I2CMSST = 0X00;//清零标志
    }
    //TM1650_Write(0x49,0x48);
    TM1650_Write(TM1650_CMD2[Brt],TM1650_CMD1);//发送命令1(0X48),再发送命令2(亮度)
    //MSBUSY指示I2C控制器是否忙碌,但本驱动程序不需要检查MSBUSY
}
//在C语言中,字符串自动转换为由二进制码组成的数组
//const char *Num 接收的是这个字符串的首地址
//Num为指针(地址),*Num为这个地址读到的东西
void TM1650_Display_Word(const char *Word){
    bit TM1650_Need_DP = 0;//是否需要小数点标志
    unsigned char TM1650_Current_Digit = 0;//记录当前写的数码管段
    unsigned char TM1650_Write_Buffer = 0;//写过程的缓存
        while(*Word != '\0' && TM1650_Current_Digit < 4){//字符串未结尾,且并没有写完完整4位
        if(*(Word+1) == '.'){//检测下一位是否是小数点(TM1650的小数点与上一个段绑定)
            TM1650_Need_DP = 1;//标记需要小数点
        }
        TM1650_Write_Buffer = Seg_Table(*Word);//按字符ASCII值查表
        if(TM1650_Need_DP){//如果需要小数点
            TM1650_Write_Buffer |= 0x80;//最高位置1
            TM1650_Need_DP = 0;//清零标志
            Word = Word + 1;//下一位存在小数点,直接跳过
            }
        switch(TM1650_Current_Digit){//写入
            case 0: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG1); break;
            case 1: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG2); break;
            case 2: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG3); break;
            case 3: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG4); break;
            }
            TM1650_Current_Digit++;//数码管段+1
            Word++;//指针+1
        }
}
void TM1650_Display_Num(unsigned int Num, unsigned char Dot_pos){
    //整数转字符所用变量
    char Trans_Temp[4];//整数截取低四位的缓存
    char TM1650_String_Buffer[8];//最终输出的字符串缓存区
    char *Ptr = TM1650_String_Buffer;//数组的指针,用于在数组取值
    unsigned char i = 4; //数据保留低四位
    unsigned char j = 0;
    unsigned char k = 0;
    //写入TM1650所用变量
    bit TM1650_Need_DP = 0;//是否需要小数点标志
    unsigned char TM1650_Current_Digit = 0;//记录当前写的数码管段
    unsigned char TM1650_Write_Buffer = 0;//写过程的缓存区域
    //整数转字符
    //先把整数拆解成纯数字数组 (倒序存放)
    //比如 1234 -> temp[0]=4, temp[1]=3, temp[2]=2, temp[3]=1
    for(k=0; k<4; k++) {
        Trans_Temp[k] = (Num % 10) + '0';
        Num /= 10;
    }
    //根据 dot_pos 组装带点的字符串
    //从高位往低位填入buffer
    for(k=0; k<4; k++) {
        TM1650_String_Buffer[j++] = Trans_Temp[3-k]; //填入数字
        if(Dot_pos == (k + 1) && k < 3) { //如果到了设定的点位
            TM1650_String_Buffer[j++] = '.';
        }
    }
    TM1650_String_Buffer[j] = '\0'; //封口
    //字符数组读取与写入显存
    while(*Ptr != '\0' && TM1650_Current_Digit < 4){//字符串未结尾,且并没有写完完整4位
        if(*(Ptr+1) == '.'){//检测下一位是否是小数点(TM1650的小数点与上一个段绑定)
            TM1650_Need_DP = 1;//标记需要小数点
        }
        TM1650_Write_Buffer = Seg_Table(*Ptr);//按字符ASCII值查表
        if(TM1650_Need_DP){//如果需要小数点
            TM1650_Write_Buffer |= 0x80;//最高位置1
            TM1650_Need_DP = 0;//清零标志
            Ptr = Ptr + 1;//下一位存在小数点,直接跳过
            }
        switch(TM1650_Current_Digit){//写入
            case 0: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG1); break;
            case 1: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG2); break;
            case 2: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG3); break;
            case 3: TM1650_Write(TM1650_Write_Buffer,TM1650_DIG4); break;
            }
            TM1650_Current_Digit++;//数码管段+1
            Ptr++;//指针+1
        }
}
#endif
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表