Skip to content

概述

TM1721 是一款带键盘扫描的 LED 段码驱动芯片(段码屏 = 显示数字的「日」字形笔画屏,每个数字由几段笔画拼成,类似老式计算器和数字电子钟的显示屏)。它一边驱动屏幕的每一段笔画亮灭,一边扫描按键(键扫 = 芯片轮流「摸」一遍按键,看哪个被按下了),一颗芯片就把「显示 + 键盘」两件事都干了。本教程使用官方 demo_TM1721 示例:初始化芯片、点亮屏幕全部段码,然后读取 6 个按键的状态并在串口打印短按 / 长按事件。

用大白话讲:段码屏就像数字电子钟的显示窗——「8」这个数字由 7 段笔画(横横竖竖组成「日」字形)拼成,TM1721 的活就是决定哪几笔亮、哪几笔灭,让笔画拼出 0~9 和字母。同时它还能一边显示一边「摸」键盘:它轮流检查每个按键有没有被按下,并把结果告诉单片机。本教程先让所有笔画都亮起来(相当于「8 8 8 8」全亮),再演示读按键。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的官方示例 applications/iot-solution/demo_TM1721 编写,代码可在本地 SDK 中直接找到。

🎯本页目标驱动 TM1721 点亮段码屏全部笔画,并读取 6 个按键的短按/长按事件(串口打印)。
🧰前置条件① Ai-WB2 开发板、基于 TM1721 的段码屏模块(带按键)、杜邦线 ② 已按 [SDK 安装](../sdk/sdk_intro) 完成开发环境搭建。
🔗相关章节GPIO 原理见 [GPIO输出(点亮LED)](../basic/gpio_led);上篇 [WS2812 RGB 灯](./ws2812)。

硬件接线

按官方示例接线(TM1721 用三根控制线 + 电源与单片机通信):

Ai-WB2 引脚 TM1721 模块
IO17 CLKPIN(时钟线,定通信节奏)
IO3 DATAPIN(数据线,传内容)
IO14 CSPIN(片选线,CS = 选中这颗芯片的「开关」,低电平选中)
3V3 VCC
GND GND

💡 三根线分别叫 CLK(时钟)、DATA(数据)、CS(片选),是一种类似 I2C 的软件模拟时序:CLK 打一个节拍、DATA 上送一个比特。片选 = 「点名」,只有被 CS 选中的芯片才会听数据线上的内容。接线时注意 IO3 同时是 SSD1306 教程 的 SDA,换外设时把线换过来即可。

进入示例工程

打开终端进入官方 demo_TM1721 示例工程目录:

cd ~/Ai-Thinker-WB2/applications/iot-solution/demo_TM1721

说明:cd 是「进入目录」命令,这里进入 demo_TM1721 工程目录;后续的 make 编译、make flash 烧录命令都必须先在这个目录里执行。

工程目录结构说明:

文件 作用
main/main.c 主程序源码,本教程主要修改的文件
components/tm1721_driver/tm1721_driver.c / .h TM1721 驱动(自研),封装初始化、写段码数据、读按键等接口
Makefile 编译入口,一般无需修改
编写代码

打开 main/main.c,本步完整代码已移至文末,见:

📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(applications/iot-solution/demo_TM1721/main/main.c)完全一致。

代码要点:

代码 作用
TM1721_init() 初始化三个控制引脚并给芯片发送开机命令,不初始化芯片不会响应
TM1721_write_data_conytinous(0, Dbyte, 14) 从地址 0 连续写 14 字节全 0xFF,把全部段码笔画点亮(8 段全亮 = 显示「8」)
TM1721_read_key_status(key_read) 读 4 字节键值,0x08/0x80 等组合对应 6 个不同按键
key_count 计数 + 判断区间 每次按键状态相同就计数,按 80~400ms 判短按、超 2.8s 判长按,只判一次会分不清长短按
vTaskDelay(portTICK_RATE_MS * 10) 每 10ms 扫一次键,太快读不准、太慢按键会被「吞」
编译工程

在工程目录执行编译:

make -j8

说明:make 是「编译工程」命令,把代码变成开发板能运行的固件(程序文件);-j8 表示用 8 个核并行编译,速度更快。

编译成功后生成固件 build_out/demo_TM1721.bin

烧录固件

开发板保持 USB 连接,确认串口设备号(Linux 下通常为 /dev/ttyUSB0),执行烧录:

make flash p=/dev/ttyUSB0 b=921600

说明:make flash 是「烧录」命令,把编译好的固件写进开发板芯片。p= 后面是串口设备号(要改成你电脑上实际的串口,可用 ls /dev/ttyUSB* 查看),b= 是烧录波特率(传输速度)。

⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式(部分开发板自动进入),等待进度条完成即烧录成功。

运行验证

烧录完成后开发板自动重启运行:

  1. 段码屏全部笔画点亮(程序把 14 个显示地址全写了 0xFF,看起来像「8 8 8 8」全亮)。
  2. 打开串口助手(波特率 921600,波特率 = 串口传输速度,收发两端必须设成一样),按下模块上的按键:
    • 短按(约 0.1~0.4 秒内按下并松开):串口打印 TIMER_KEY SHORT PRESS 等(哪个键就打印哪个键名);
    • 长按(按住超过约 2.8 秒):串口打印 TIMER_KEY LONG PRESS
    • 松开瞬间还会打印一行 get key_press value: 01(键值,每 10ms 打印一次)。

💡 6 个按键分别对应 TIMER_KEY / CONFIRM_KEY / SWITCH_KEY / MODE_KEY / UP_KEY / DOWN_KEY。如果按键后打印 prevent from wrong detect,说明按键时长没落在判定区间(太快或太慢),多试几次即可。

看到屏幕全亮、按键后串口打印对应键名和 SHORT/LONG PRESS 即为成功;如果屏幕不亮或按键无打印,说明还没成功,对照文末「常见问题与踩坑提示」排查。

代码执行流程

例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):


本文 API 汇总

TM1721_init()

初始化 TM1721 芯片:把 CLK(IO17)、DATA(IO3)、CS(IO14)三个引脚配置为输出,并向芯片发送开机/设置命令(驱动自研接口,源码见 components/tm1721_driver/tm1721_driver.c)。

参数:无

返回值:无

TM1721_write_data_conytinous(first_addr, Dbyte, addr_num)

从指定地址开始连续写入多个地址的段码显示数据(驱动自研接口,源码见 components/tm1721_driver/tm1721_driver.c)。每个地址 1 字节,对应 8 段笔画(位 a~h),写 0xFF 即 8 段全亮。

参数

  • first_addr:起始显示地址,可选值:0x00~0x0F,本教程 0
  • Dbyte:要写入的数据数组指针(每元素 1 字节段码),本教程全 0xFF
  • addr_num:连续写入的地址个数,可选值:1~14,本教程 14(首地址 + 数量不能超过 0x0F

返回值:成功返回 1;参数非法(地址越界等)返回 0

TM1721_read_key_status(data)

读取按键扫描结果(驱动自研接口,源码见 components/tm1721_driver/tm1721_driver.c)。芯片把 6 个按键的状态编码在 4 字节里返回,程序通过比较 0x08 / 0x80 等键值判断是哪个键。

参数

  • data:4 字节接收缓冲区指针,返回后含键值(如 data[0] == 0x08 表示 TIMER 键)

返回值:读取成功返回 1

vTaskDelay(ms)

让当前任务挂起指定毫秒数,期间让出 CPU 给其他任务(FreeRTOS 系统 API)。

参数

  • ms:延时毫秒数,可选值:任意非负整数(本教程按键扫描周期 portTICK_RATE_MS * 10 = 10ms)

返回值:无


完整代码

以下为 main/main.c 完整源码,与官方示例(applications/iot-solution/demo_TM1721/main/main.c)完全一致:

📜 点击展开 main/main.c 完整代码
c
#include <stdio.h>
#include <string.h>
#include <FreeRTOS.h>
#include <task.h>
#include "bl_sys.h"
#include "tm1721_driver.h"
#include <aos/kernel.h>
#include <bl_gpio.h>
#include <bl602_gpio.h>
#include <hosal_gpio.h>

#define TM1721_OPERAT_CMD 0x04
#define TM1721_DATA_CMD 0x40
#define TM1721_ADDR_CMD 0xC0
#define TM1721_SHOW_CMD 0x9F

#define SHORT_PRESS_TIMES_MIN 8
#define SHORT_PRESS_TIMES_MAX 40
#define LONG_PRESS_TIMES 280

typedef enum
{
    NONE_KEY = 0,
    TIMER_KEY = 1,
    CONFIRM_KEY,
    SWITCH_KEY,
    MODE_KEY,
    UP_KEY,
    DOWN_KEY
} key_confirm_t;
// LED----------------------------- a  b  c  d  e  f  g  h
static uint8_t NumberToLED[][8] = {{1, 1, 1, 1, 1, 1, 0, 0},  // 0
                                   {0, 1, 1, 0, 0, 0, 0, 0},  // 1
                                   {1, 1, 0, 1, 1, 0, 1, 0},  // 2
                                   {1, 1, 1, 1, 0, 0, 1, 0},  // 3
                                   {0, 1, 1, 0, 0, 1, 1, 0},  // 4
                                   {1, 0, 1, 1, 0, 1, 1, 0},  // 5
                                   {1, 0, 1, 1, 1, 1, 1, 0},  // 6
                                   {1, 1, 1, 0, 0, 0, 0, 0},  // 7
                                   {1, 1, 1, 1, 1, 1, 1, 0},  // 8
                                   {1, 1, 1, 1, 0, 1, 1, 0},  // 9
                                   {1, 0, 0, 1, 1, 1, 1, 0},  // E
                                   {0, 0, 0, 0, 0, 0, 1, 0}}; // -

void screen_bit_show_step_to_step()
{
    TM1721_init();
    uint8_t byte = 0xFF;
    uint8_t *Dbyte = calloc(1, sizeof(uint8_t) * 14);
    uint8_t *ADDR = calloc(1, sizeof(uint8_t) * 15);
    for (int i = 0; i < 16; i++)
    {
        ADDR[i] = i;
    }
    memset(Dbyte, 0, sizeof(uint8_t) * 14);
    TM1721_write_data_conytinous(0, Dbyte, 14);
    TM1721_write_data_conytinous(14, Dbyte, 2);
    vTaskDelay(portTICK_RATE_MS * 1000);
    byte = 0;
    for (int i = 0; i < 15; i++)
    {
        for (int j = 0; j < 8; j++)
        {
            byte = byte + (1 << j);
            TM1721_write_data_conytinous(i, &byte, 1);
            vTaskDelay(portTICK_RATE_MS * 500);
        }
        byte = 0;
    }
    byte = 0x0F;
    TM1721_write_data_conytinous(15, &byte, 1);
}

void main(void)
{
    uint8_t *key_read = calloc(1, sizeof(uint8_t) * 4);
    uint8_t *Dbyte = calloc(1, sizeof(uint8_t) * 16);
    key_confirm_t key_press = NONE_KEY;
    key_confirm_t key_front = NONE_KEY;
    uint8_t key_count = 0;
    TM1721_init();
    memset(Dbyte, 0xFF, 16);
    TM1721_write_data_conytinous(0, Dbyte, 14);
    TM1721_write_data_conytinous(14, Dbyte + 14, 2);
    vTaskDelay(portTICK_RATE_MS * 100);

    while (1)
    {
        TM1721_read_key_status(key_read);
        if (key_read[0] == 0x08)
        {
            key_press = TIMER_KEY;
            if (TIMER_KEY == key_front)
            {
                key_count++;
                if (key_count > LONG_PRESS_TIMES)
                {
                    printf("TIMER_KEY LONG PRESS\r\n");
                }
            }
        }
        else if (key_read[0] == 0x80)
        {
            key_press = CONFIRM_KEY;
            if (CONFIRM_KEY == key_front)
            {
                key_count++;
                if (key_count > LONG_PRESS_TIMES)
                {
                    printf("CONFIRM_KEY LONG PRESS\r\n");
                }
            }
        }
        else if (key_read[2] == 0x80)
        {
            key_press = SWITCH_KEY;
            if (SWITCH_KEY == key_front)
            {
                key_count++;
                if (key_count > LONG_PRESS_TIMES)
                {
                    printf("SWITCH_KEY LONG PRESS\r\n");
                }
            }
        }
        else if (key_read[2] == 0x08)
        {
            key_press = MODE_KEY;
            if (MODE_KEY == key_front)
            {
                key_count++;
                if (key_count > LONG_PRESS_TIMES)
                {
                    printf("MODE_KEY LONG PRESS\r\n");
                }
            }
        }
        else if (key_read[1] == 0x80)
        {
            key_press = UP_KEY;
            if (UP_KEY == key_front)
            {
                key_count++;
                if (key_count > LONG_PRESS_TIMES)
                {
                    printf("UP_KEY LONG PRESS\r\n");
                }
            }
        }
        else if (key_read[1] == 0x08)
        {
            key_press = DOWN_KEY;
            if (DOWN_KEY == key_front)
            {
                key_count++;
                if (key_count > LONG_PRESS_TIMES)
                {
                    printf("DOWN_KEY LONG PRESS\r\n");
                }
            }
        }
        else
        {
            key_press = 0;
            if (key_count > SHORT_PRESS_TIMES_MIN && key_count < SHORT_PRESS_TIMES_MAX)
            {
                switch (key_front)
                {
                case TIMER_KEY:
                    printf("TIMER_KEY SHORT PRESS\r\n");
                    break;
                case CONFIRM_KEY:
                    printf("CONFIRM_KEY SHORT PRESS\r\n");
                    break;
                case SWITCH_KEY:
                    printf("SWITCH_KEY SHORT PRESS\r\n");
                    break;
                case MODE_KEY:
                    printf("MODE_KEY SHORT PRESS\r\n");
                    break;
                case UP_KEY:
                    printf("UP_KEY SHORT PRESS\r\n");
                    break;
                case DOWN_KEY:
                    printf("DOWN_KEY SHORT PRESS\r\n");
                    break;
                default:
                    break;
                }
            }
            else
            {
                printf("prevent from wrong detect\r\n");
            }
        }
        key_count = 0;
        key_front = key_press;
        vTaskDelay(portTICK_RATE_MS * 10);
        printf("get key_press value: %02X\r\n", key_press);
    }
}

常见问题与踩坑提示

⚠️ 段码屏完全不亮
原因:CLK/DATA/CS 三根线接反、未共地或供电不足
解决:按官方接线 IO17=CLKPIN、IO3=DATAPIN、IO14=CSPIN,VCC 接 3V3 且 GND 共地;杜邦线重新插紧

⚠️ 按键后打印 prevent from wrong detect
原因:按键时长不在判定区间——太短(<80ms)或太长(超过 400ms 又被判成别的)
解决:用「快速按下立刻松开」的方式操作;想调整灵敏度可改 main.cSHORT_PRESS_TIMES_MIN/MAXLONG_PRESS_TIMES 三个宏(宏 = 给数字起的有名字的代号)

⚠️ 想显示数字而不是全亮
原因main.c 里把所有地址写成了 0xFF(全亮),官方把 0~9 的段码表 NumberToLED 也放在 main.c 里但未启用
解决:参考 NumberToLED 表,把对应数字的 8 段笔画拼成字节(如数字 0 = 段 a~f 亮 = 0x3F)再调用 TM1721_write_data_conytinous 写入

⚠️ 编译报错找不到 tm1721_driver.h
原因:驱动在 components/ 子目录,编译配置未包含或目录路径不对
解决:确认在工程根目录 ~/Ai-Thinker-WB2/applications/iot-solution/demo_TM1721 执行 make -j8make 会自动编译 components 下的驱动,不要手动 cd 进 main/ 目录

⚠️ 烧录时提示无法打开串口 / 一直卡住等待
原因:串口设备号不对、权限不足,或未进入下载模式、数据线只能充电
解决:确认设备号 ls /dev/ttyUSB*,权限不足执行 sudo usermod -aG dialout $USER;烧录时按提示长按 EN 键,换一根能传数据的 Type-C 数据线后重试

运行自检

段码屏全部笔画点亮(呈「8 8 8 8」全亮效果),短按/长按模块按键时串口分别打印 XXX_KEY SHORT PRESS / XXX_KEY LONG PRESS,即 TM1721 驱动验证通过。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

Released under the MIT License. Build Time 2026-09-11 14:52:23