七段数码管应该是个入门级的外设了, 用单片机管脚直接扫描驱动, 或者用TM1650之类专用IC驱动都不难, 但是直接扫描驱动时写起来还是比较繁琐, 以及换个单片机基本得重写一遍.
前几天做了个电压表模块,
张浩:这次做个简单的, 3位电压表模块zhuanlan.zhihu.com顺便实现了一个硬件无关的数码管驱动, 以后再折腾这东西就省事多了. 源代码如下:
zss.h:
#ifndef _ZSS_H
#define _ZSS_H
typedef struct {
void (*set_segment_f)(unsigned int segment, int state); // 写段码
void (*set_digit_f)(unsigned int digit, int state); // 写位码
int digits, segments;
} zss_cfg_t;
void zss_update(void); // 刷新, 每1~10ms调用一次
void zss_init(zss_cfg_t* cfg);
void zss_set_num(int num); // 显示数值
void zss_set_dot(int pos); // 设置小数点位置
void zss_set_raw(unsigned char* raw); // 显示自定义字符
#endif
zss.c:
#include "zss.h"
enum {
ZSS_MODE_RAW, ZSS_MODE_NUM
};
static const unsigned char masks[10] = { //
0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f // char mask of digit 0~9
};
static struct {
zss_cfg_t cfg;
int num;
unsigned char mode, dot_pos;
unsigned char* raw;
} g;
void zss_set_num(int num)
{
g.mode = ZSS_MODE_NUM;
g.num = num;
}
void zss_update(void)
{
static int curr_digit = 0;
int i, j, n;
static int num;
for(i = 0; i < g.cfg.digits; i++)
g.cfg.set_digit_f(i, 1); // 先全部关闭
switch(g.mode) {
case ZSS_MODE_NUM:
if(curr_digit == 0)
num = g.num;
else
num /= 10;
for(j = 0; j < g.cfg.segments; j++) {
if(masks[num % 10] & (1 << j))
g.cfg.set_segment_f(j, 1);
else
g.cfg.set_segment_f(j, 0);
}
if(curr_digit == g.dot_pos)
g.cfg.set_segment_f(7, 1);
else
g.cfg.set_segment_f(7, 0);
g.cfg.set_digit_f(g.cfg.digits - curr_digit - 1, 0); // 数字从右向左显示
break;
case ZSS_MODE_RAW:
for(j = 0; j < g.cfg.segments; j++) {
if(g.raw[curr_digit] & (1 << j))
g.cfg.set_segment_f(j, 1);
else
g.cfg.set_segment_f(j, 0);
}
g.cfg.set_digit_f(curr_digit, 0);
break;
}
curr_digit++;
curr_digit %= g.cfg.digits;
}
void zss_set_dot(int pos)
{
g.dot_pos = pos;
}
void zss_set_raw(unsigned char* raw)
{
g.raw = raw;
g.mode = ZSS_MODE_RAW;
}
void zss_init(zss_cfg_t* cfg)
{
g.cfg = *cfg;
}
用户需要提供两个回调函数来向段码/位码的管脚写1或写0, 在STM8S平台的代码如下, 其他平台上稍微改改就行了. 每个管脚独立设置所在的GPIO组, 虽然效率低一些, 但是不需要把8个段码放在同一组GPIO上, 这样布线可以就近走线, 轻松多了. 我这次用的是共阴数码管, 点亮时是段码写1, 位码写0. 如果是共阳数码管, 在回调函数里的if(state)这里, state前面加个感叹号就行.
typedef struct {
GPIO_TypeDef* gpiox;
GPIO_Pin_TypeDef pin;
} gpio_t;
const gpio_t segments[8] = {
//
{GPIOD, GPIO_PIN_4}, //
{GPIOC, GPIO_PIN_4}, //
{GPIOC, GPIO_PIN_7}, //
{GPIOD, GPIO_PIN_2}, //
{GPIOD, GPIO_PIN_3}, //
{GPIOD, GPIO_PIN_5}, //
{GPIOC, GPIO_PIN_6}, //
{GPIOC, GPIO_PIN_5} //
};
const gpio_t digits[3] = {
//
{GPIOA, GPIO_PIN_3}, //
{GPIOB, GPIO_PIN_5}, //
{GPIOB, GPIO_PIN_4} //
};
void set_digit(unsigned int digit, int state)
{
if(digit < 3) {
if(state)
digits[digit].gpiox->ODR |= digits[digit].pin;
else
digits[digit].gpiox->ODR &= ~(digits[digit].pin);
}
}
void set_segment(unsigned int segment, int state)
{
if(segment < 8) {
if(state)
segments[segment].gpiox->ODR |= segments[segment].pin;
else
segments[segment].gpiox->ODR &= ~(segments[segment].pin);
}
}
初始化, 提供回调函数指针, 并且设定最大段数和位数即可. 想想其实可以改成把字符mask也一起作为参数传入, 这样要换九段管米字管之类也容易了.
zss_cfg_t cfg;
cfg.set_digit_f = set_digit;
cfg.set_segment_f = set_segment;
cfg.digits = 3;
cfg.segments = 8;
zss_init(&cfg);
使用时每1~10ms调用zss_update()函数刷新, 需要显示数值时用zss_set_num(), 指定小数点位置用zss_set_dot(), 显示HI, LO, Err之类字符时用zss_set_raw()即可.
Github链接:
https://github.com/tomzbj/zssgithub.com