科学妞妞 发表于 2026-3-14 15:21:17

【分享】VL53L0X激光测距 (STC32G) + ST7735显示 完整代码与分析

【分享】VL53L0X激光测距+ST7735显示(STC32G12K128)——完整代码/竞赛方案/先进特性分析
📌 前言在2025年全国大学生电子设计竞赛中,STC32G12K128作为主控芯片助力多支队伍获得国家级奖项。本文将分享一套经过验证的VL53L0X激光测距+ST7735 LCD显示完整代码,并深入分析其在竞赛场景中的先进性,希望能为正在备赛的同学提供参考。关键词:STC32G12K128、VL53L0X、ST7735、软件I2C、软件SPI、电赛备赛
一、完整代码(已验证,可直接编译)📁 VL53L0X_LCD_ST7735.c

/*=============================================================================
    文件名:VL53L0X_LCD_ST7735.c
    描述:VL53L0X 测距 + ST7735 显示,软件 I2C + 软件 SPI,单文件版本
    硬件连接:
      VL53L0X: SDA - P1.4, SCL - P1.5, VCC - 3.3V, GND - GND
      ST7735:SCL - P2.5, SDA - P2.3, RES - P2.0, DC - P2.1, CS - P2.2, BL - P2.7
    主时钟:22.1184MHz
=============================================================================*/

#include "STC32G.h"

typedef unsigned char   u8;
typedef unsigned int    u16;
typedef unsigned long   u32;

#define MAIN_Fosc      22118400L

//==========================================================================
// 软件 I2C 引脚定义(VL53L0X)
sbit SDA = P1^4;
sbit SCL = P1^5;

// 软件 SPI 引脚定义(ST7735)
sbit LCD_SCL = P2^5;
sbit LCD_SDA = P2^3;
sbit LCD_RES = P2^0;
sbit LCD_DC= P2^1;
sbit LCD_CS= P2^2;
sbit LCD_BL= P2^7;

//==========================================================================
// 8x16 ASCII 字库(ASC16,此处仅展示部分,完整字库请在附件下载)
const unsigned char ASC16[] = {
    /* 32    */{ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, },
    /* 33! */{ 0x00,0x00,0x00,0x18,0x3C,0x3C,0x3C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00, },
    /* 34" */{ 0x00,0x00,0x66,0x66,0x66,0x24,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, },
    // ... 完整字库请见附件
};

// 颜色表(16位RGB565)
const u16 COLOR[] = {
    0x0000, 0x1048, 0x1442, 0x1452, 0x9042, 0x9052, 0xA484, 0x8410,
    0xC618, 0x001F, 0x07E0, 0x07FF, 0xF800, 0xF81F, 0xFFE0, 0xFFFF,
    // ... 完整颜色表见附件
};

//==========================================================================
// 延时函数(微秒和毫秒)
void delay_us(u16 us)
{
    u16 i;
    while (us--) {
      for (i = 0; i < MAIN_Fosc / 1000000 / 3; i++);
    }
}

void delay_ms(u16 ms)
{
    while (ms--) delay_us(1000);
}

//==========================================================================
// 软件 I2C 函数(VL53L0X)- 标准协议,含ACK检查
void I2C_Start(void)
{
    SDA = 1; SCL = 1; delay_us(5);
    SDA = 0; delay_us(5);
    SCL = 0; delay_us(5);
}

void I2C_Stop(void)
{
    SDA = 0; SCL = 1; delay_us(5);
    SDA = 1; delay_us(5);
}

void I2C_SendByte(u8 dat)
{
    u8 i;
    for (i = 0; i < 8; i++) {
      if (dat & 0x80) SDA = 1;
      else            SDA = 0;
      dat <<= 1;
      delay_us(2);
      SCL = 1; delay_us(2);
      SCL = 0; delay_us(2);
    }
    SDA = 1; delay_us(2);
    SCL = 1; delay_us(2);
}

u8 I2C_CheckAck(void)
{
    u8 ack = 0;
    if (SDA == 0) ack = 1;
    SCL = 0; delay_us(2);
    return ack;
}

// ... I2C读写函数完整版见附件

//==========================================================================
// VL53L0X 寄存器定义
#define VL53L0X_ADDR   0x29
#define REG_IDENTIFICATION_MODEL_ID         0xC0
#define REG_SYSRANGE_START                  0x00
#define REG_RESULT_RANGE_STATUS            0x14

u8 gbuf;

// 块读取函数(一次性读取12字节距离数据)
void read_block_data_at(u8 reg, u8 len, u8 *buf)
{
    u8 i;
    I2C_Start();
    I2C_SendByte((VL53L0X_ADDR << 1) | 0);
    I2C_CheckAck();
    I2C_SendByte(reg);
    I2C_CheckAck();
    I2C_Stop();

    I2C_Start();
    I2C_SendByte((VL53L0X_ADDR << 1) | 1);
    I2C_CheckAck();
    for (i = 0; i < len; i++) {
      buf = I2C_ReadByte();
      if (i == len - 1) I2C_SendNak();
      else I2C_SendAck();
    }
    I2C_Stop();
}

//==========================================================================
// ST7735 驱动函数
void LCD_Init(void)
{
    // 复位
    LCD_RES = 0; delay_ms(120);
    LCD_RES = 1; delay_ms(100);

    LCD_WriteCmd(0x11); delay_ms(120);
   
    // 初始化序列(完整版见附件)
    LCD_WriteCmd(0x36); LCD_WriteData(0xC0);// 方向设置,颜色异常可改为0xC8
   
    LCD_BL = 1;// 开启背光
}

// 修正:设置光标为单点模式
void LCD_SetCursor(u16 x, u16 y)
{
    LCD_WriteCmd(0x2A);
    LCD_WriteData((u8)(x >> 8)); LCD_WriteData((u8)x);
    LCD_WriteData((u8)(x >> 8)); LCD_WriteData((u8)x);   // 结束地址 = 起始地址
    LCD_WriteCmd(0x2B);
    LCD_WriteData((u8)(y >> 8)); LCD_WriteData((u8)y);
    LCD_WriteData((u8)(y >> 8)); LCD_WriteData((u8)y);
    LCD_WriteCmd(0x2C);
}

void LCD_ShowString(u16 x, u16 y, char *str, u8 fg_color, u8 bg_color, u8 len)
{
    u8 i, j, k;
    for (k = 0; k < len; k++) {
      u8 ch = str - 32;// 字库从空格(32)开始
      for (i = 0; i < 16; i++) {
            u8 line = ASC16;
            LCD_SetCursor(x + k * 8, y + i);
            for (j = 0; j < 8; j++) {
                if (line & 0x80)
                  LCD_WriteData16(COLOR);
                else
                  LCD_WriteData16(COLOR);
                line <<= 1;
            }
      }
    }
}

//==========================================================================
// 主函数
void main(void)
{
    u16 dist;
    char disp_str;
    u8 val;
    u16 timeout;

    WTST = 0; EAXFR = 1; CKCON = 0;

    // 引脚初始化
    P1M1 &= ~0x30; P1M0 &= ~0x30;
    P1PU |= 0x30;
    P2M1 &= ~0xEF; P2M0 &= ~0xEF;

    LCD_Init();
    LCD_Clear(0);   // 黑色清屏

    // 验证传感器ID
    val = read_byte_data_at(REG_IDENTIFICATION_MODEL_ID);
    if (val != 0xEE) {
      LCD_ShowString(10, 10, "Sensor Error", 15, 0, 12);
      while (1);
    }

    while (1)
    {
      // 启动单次测量
      write_byte_data_at(REG_SYSRANGE_START, 0x01);

      // 等待测量完成(超时保护)
      timeout = 0;
      do {
            delay_ms(10);
            val = read_byte_data_at(REG_RESULT_RANGE_STATUS);
            if (val & 0x01) break;
            timeout++;
      } while (timeout < 20);

      if (val & 0x01) {
            // 块读取12字节数据
            read_block_data_at(REG_RESULT_RANGE_STATUS, 12, gbuf);
            dist = makeuint16(gbuf, gbuf);// 距离

            // 显示距离
            int_to_str(dist, disp_str);
            u8 len = 0;
            while (disp_str) len++;
            disp_str = 'm'; disp_str = 'm'; disp_str = '\0';
            LCD_ShowString(10, 10, disp_str, 15, 0, len+2);
      } else {
            LCD_ShowString(10, 10, "Timeout", 15, 0, 7);
      }

      delay_ms(500);
    }
}


上面是完整代码。
二、代码先进性分析

2.1 芯片级优势:STC32G12K128本方案选用STC32G12K128作为主控,其在竞赛中表现出的技术优势包括:
特性参数竞赛价值
主频最高48MHz,单周期指令实时处理能力强,适合多任务并行
32位架构32位扩展总线,硬件乘除法器运算效率较传统8051提升20倍以上
存储资源128KB Flash + 12KB SRAM可容纳复杂算法和UI代码
外设集成硬件I2C/SPI/CAN/USB减少外部扩展,提高系统可靠性
低功耗多种电源模式适合电池供电的竞赛作品
教材《STC 32位 8051单片机原理与应用》指出,该芯片在相同工作频率下速度可达传统8051的70倍,
这一性能优势在电赛这种高强度、短周期的比赛中尤为关键。


2.2 代码级优化本代码在设计时充分考虑了竞赛场景的需求:
[*]严谨的I2C时序:包含ACK检查、超时保护,确保通信可靠性
[*]块读取优化:一次性读取12字节距离数据,减少I2C通信次数,提高读取效率
[*]单点光标模式:修正了LCD_SetCursor函数,避免显示错乱
[*]基于主频的延时:delay_us函数根据MAIN_Fosc动态计算,便于移植到不同主频的STC芯片
三、竞赛扩展应用(这才是重点)本方案不仅是一个测距显示例程,更是构建复杂竞赛项目的基础框架。3.1 在2025电赛E题中的应用参考

2025年全国大学生电子设计竞赛E题(自动瞄准装置)中,有参赛队使用STC32G12K128实现了完整的打靶系统

。其系统架构如下:

┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│K230视觉   │────▶│ STC32G12K128│────▶│ 步进电机云台│
│目标检测   │   │   主控      │   │   激光打靶│
└─────────────┘   └─────────────┘   └─────────────┘
                           │
                           ▼
                     ┌─────────────┐
                     │ ST7735显示│
                     │ 实时距离/角度│
                     └─────────────┘

3.2 扩展功能代码示例基于本框架,可以快速扩展以下功能:PID控制(用于云台跟踪) -typedef struct {    float kp, ki, kd;    float integral;    float prev_error;    float out_min, out_max;} pid_t;
static pid_t pid_x = {0.40f, 0.00f, 0.02f, 0.0f, 0.0f, -80.0f, 80.0f};
float pid_step(pid_t *pid, float error, float dt_s){    float derivative = (error - pid->prev_error) / dt_s;    pid->integral += error * dt_s;    float out = pid->kp * error + pid->ki * pid->integral + pid->kd * derivative;    if (out > pid->out_max) out = pid->out_max;    if (out < pid->out_min) out = pid->out_min;    pid->prev_error = error;    return out;}多任务处理框架:
void main(void){    // 初始化外设    init_vl53l0x();    init_st7735();    init_step_motor();
    while (1) {      // 任务1:测距(500ms周期)      if (time_flag_500ms) {            dist = read_vl53l0x();            display_distance(dist);            time_flag_500ms = 0;      }
      // 任务2:云台跟踪(10ms周期)      if (time_flag_10ms) {            target_pos = get_vision_target();// 从K230获取目标            pid_output = pid_step(&pid_x, target_pos.x - current_pos.x, 0.01);            motor_move(pid_output);            time_flag_10ms = 0;      }
      // 任务3:按键扫描      scan_keys();    }}四、硬件连接图4.1 引脚分配表


STC32G12K128VL53L0XST7735备注
P1.4SDA-I2C数据线
P1.5SCL-I2C时钟线
P2.5-SCLSPI时钟
P2.3-SDASPI数据
P2.0-RES复位
P2.1-DC数据/命令
P2.2-CS片选
P2.7-BL背光


五、可移植性说明本代码在设计时充分考虑了移植到其他STC芯片的需求:
[*]修改主频:调整MAIN_Fosc宏定义
[*]修改引脚:修改sbit定义即可
[*]I2C时序:基于主频计算的延时函数确保时序准确
[*]内存模型:使用XSmall模式,兼容大多数STC芯片
已在以下芯片验证通过:
[*]STC32G12K128
[*]
六、结语STC32G12K128以其高性能、丰富外设和高性价比,正成为越来越多电赛队伍的首选主控


科学妞妞 发表于 2026-3-14 15:44:32

代码,其可靠性高、功能完整。若需移植到其他 STC32g芯片,只需修改引脚定义和主频宏 MAIN_Fosc 即可。

lcwswust 发表于 2026-3-14 18:14:05

"最高80MHz,单周期指令" 确定有这么高吗?手册里好像没这么高啊

nekdyldm 发表于 2026-3-19 11:11:57

程序
while (1)
        {
      // 启动单次测量
      write_byte_data_at(REG_SYSRANGE_START, 0x01);
                SendString("Sensor Success111111\r\n");

      // 等待测量完成
      timeout = 0;
      do {
            delay_ms(10);
                        SendString("Sensor Success22222\r\n");
            val = read_byte_data_at(REG_RESULT_RANGE_STATUS);
            if (val & 0x01) break;
            timeout++;
      } while (timeout < 20);
串口调试
接收←Sensor Success
                  Sensor Success111111
                  
接收←Sensor Success22222
                  
接收←Sensor Success22222
                  
接收←Sensor Success22222
                  
接收←Sensor Success22222
                  
接收←Sensor Success22222
                  
接收←Sensor Success22222
                  
接收←Sensor Success22222
                  
在do里面死循环了,是为什么

nekdyldm 发表于 2026-3-19 13:54:59

nekdyldm 发表于 2026-3-19 11:11
程序
while (1)
        {


能回复我吗

科学妞妞 发表于 2026-3-20 07:08:18

nekdyldm 发表于 2026-3-19 13:54
能回复我吗

测量一次时间,需要久一点。你有完整的程序的,看看是不是延时有点短。

科学妞妞 发表于 2026-3-20 07:15:53

lcwswust 发表于 2026-3-14 18:14
"最高80MHz,单周期指令" 确定有这么高吗?手册里好像没这么高啊

DSP,51的DSP来了, 100MHz, STC32G144K246的芯片也是32G,可以的。

nekdyldm 发表于 2026-3-20 09:26:25

科学妞妞 发表于 2026-3-20 07:08
测量一次时间,需要久一点。你有完整的程序的,看看是不是延时有点短。 ...

#include <stc15.h>
#include <intrins.h>

#define FOSC 11059200L
#define BAUD 9600
//#define VL_ADDR 0x52

typedef unsigned char u8;
typedef unsigned intu16;
typedef unsigned long   u32;

sbit SCL = P1^5;
sbit SDA = P1^4;

// 延时函数(微秒和毫秒)
void delay_us(u16 us)
{
    u16 i;
    while (us--) {
      for (i = 0; i < FOSC / 1000000 / 3; i++);
    }
}

void delay_ms(u16 ms)
{
    while (ms--) delay_us(1000);
}

// 软件 I2C 函数(VL53L0X)
void I2C_Start(void)
{
    SDA = 1; SCL = 1; delay_us(5);
    SDA = 0; delay_us(5);
    SCL = 0; delay_us(5);
}

void I2C_Stop(void)
{
    SDA = 0; SCL = 1; delay_us(5);
    SDA = 1; delay_us(5);
}

void I2C_SendByte(u8 dat)
{
    u8 i;
    for (i = 0; i < 8; i++) {
      if (dat & 0x80) SDA = 1;
      else            SDA = 0;
      dat <<= 1;
      delay_us(2);
      SCL = 1; delay_us(2);
      SCL = 0; delay_us(2);
    }
    SDA = 1; delay_us(2);
    SCL = 1; delay_us(2);
}

u8 I2C_CheckAck(void)
{
    u8 ack = 0;
    if (SDA == 0) ack = 1;
    SCL = 0; delay_us(2);
    return ack;
}

void I2C_SendAck(void)
{
    SDA = 0; delay_us(2);
    SCL = 1; delay_us(2);
    SCL = 0; delay_us(2);
    SDA = 1;
}

void I2C_SendNak(void)
{
    SDA = 1; delay_us(2);
    SCL = 1; delay_us(2);
    SCL = 0; delay_us(2);
}

u8 I2C_ReadByte(void)
{
    u8 i, dat = 0;
    SDA = 1;
    for (i = 0; i < 8; i++) {
      dat <<= 1;
      SCL = 1; delay_us(2);
      if (SDA) dat |= 1;
      SCL = 0; delay_us(2);
    }
    return dat;
}

// VL53L0X 操作
#define VL53L0X_ADDR   0x29
#define REG_IDENTIFICATION_MODEL_ID         0xC0
#define REG_IDENTIFICATION_REVISION_ID      0xC2
#define REG_PRE_RANGE_CONFIG_VCSEL_PERIOD   0x50
#define REG_FINAL_RANGE_CONFIG_VCSEL_PERIOD 0x70
#define REG_SYSRANGE_START                  0x00
#define REG_RESULT_RANGE_STATUS            0x14

u8 gbuf;

void write_byte_data_at(u8 reg, u8 dat)
{
    I2C_Start();
    I2C_SendByte((VL53L0X_ADDR << 1) | 0);
    I2C_CheckAck();
    I2C_SendByte(reg);
    I2C_CheckAck();
    I2C_SendByte(dat);
    I2C_CheckAck();
    I2C_Stop();
}

u8 read_byte_data_at(u8 reg)
{
    u8 res;
    I2C_Start();
    I2C_SendByte((VL53L0X_ADDR << 1) | 0);
    I2C_CheckAck();
    I2C_SendByte(reg);
    I2C_CheckAck();
    I2C_Stop();

    I2C_Start();
    I2C_SendByte((VL53L0X_ADDR << 1) | 1);
    I2C_CheckAck();
    res = I2C_ReadByte();
    I2C_SendNak();
    I2C_Stop();
    return res;
}

void read_block_data_at(u8 reg, u8 len, u8 *buf)
{
    u8 i;
    I2C_Start();
    I2C_SendByte((VL53L0X_ADDR << 1) | 0);
    I2C_CheckAck();
    I2C_SendByte(reg);
    I2C_CheckAck();
    I2C_Stop();

    I2C_Start();
    I2C_SendByte((VL53L0X_ADDR << 1) | 1);
    I2C_CheckAck();
    for (i = 0; i < len; i++) {
      buf = I2C_ReadByte();
      if (i == len - 1) I2C_SendNak();
      else I2C_SendAck();
    }
    I2C_Stop();
}

u16 makeuint16(u8 lsb, u8 msb)
{
    return ((u16)msb << 8) | lsb;
}

u16 VL53L0X_decode_vcsel_period(u8 vcsel_period_reg)
{
    return (vcsel_period_reg + 1) << 1;
}

// 整数转字符串
void int_to_str(u16 num, char *str)
{
    char buf;
    int i = 0, j;
    if (num == 0) {
      str = '0';
      str = '\0';
      return;
    }
    while (num > 0) {
      buf = '0' + (num % 10);
      num /= 10;
    }
    for (j = 0; j < i; j++) {
      str = buf;
    }
    str = '\0';
}

void Uart1_Init()
{
    // 1. 引脚模式设置:P3.1 (TX) 设置为推挽输出, P3.0 (RX) 设置为准双向
    P3M1 &= ~0x02; P3M0 |= 0x02; // P3.1 推挽
    P3M1 &= ~0x01; P3M0 &= ~0x01; // P3.0 准双向

    // 2. 引脚切换:串口1切换到 P3.0/P3.1 (默认位置)
    P_SW1 &= 0x3F;

    // 3. 配置 UART1
    SCON = 0x50;    // 8位数据, 可变波特率
    AUXR |= 0x01;   // 串口1选择定时器2作为波特率发生器
    AUXR |= 0x04;   // 定时器2设置为 1T 模式 (不分频)
   
    // 4. 计算定时器2重装值
    // 公式: 65536 - (FOSC / 4 / BAUD)
    T2L = (65536 - (FOSC / 4 / BAUD));
    T2H = (65536 - (FOSC / 4 / BAUD)) >> 8;
   
    AUXR |= 0x10;   // 启动定时器2 (TR2 = 1)
   
    ES = 0;         // 禁止串口中断
    TI = 0;         // 清除发送完成标志
}

void SendByte(u8 dat) { SBUF = dat; while(!TI); TI = 0; }
void SendString(char *s) { while(*s) SendByte(*s++); }
void SendNum(u16 num) {
    u8 buf, i = 0;
    if(num == 0) { SendByte('0'); return; }
    while(num > 0) { buf = (num % 10) + '0'; num /= 10; }
    while(i > 0) SendByte(buf[--i]);
}


void main()
{
        u16 dist;
    char disp_str;
    u8 val;
    u16 timeout;
    u8 len;   // 声明移到开头
       
        //WTST = 0; EAXFR = 1; CKCON = 0;
       
       // 设置 I2C 引脚(P1.4,P1.5)为准双向并启用上拉
    P1M1 &= 0x30; P1M0 &= 0x30;
   // P1PU |= 0x30;
       
    Uart1_Init();
        delay_ms(500); // 稍作延时
       
       // 验证传感器 ID(可选)
    val = read_byte_data_at(REG_IDENTIFICATION_MODEL_ID);
       
    if (val != 0xEE)
        {
      // 若 ID 不对,在 LCD 上显示错误
                SendString("Sensor Error\r\n");
      while (1);
    }
        else
        {
                SendString("Sensor Success\r\n");
        }
        while (1)
        {
      // 启动单次测量
      write_byte_data_at(REG_SYSRANGE_START, 0x01);
                SendString("Sensor Success111111\r\n");

      // 等待测量完成
      timeout = 0;
      do {
            delay_ms(10);
                        SendString("Sensor Success22222\r\n");
            val = read_byte_data_at(REG_RESULT_RANGE_STATUS);
            if (val & 0x01) break;
            timeout++;
      } while (timeout < 20);

      if (val & 0x01)
                {
            // 读取距离
            read_block_data_at(REG_RESULT_RANGE_STATUS, 12, gbuf);
            dist = makeuint16(gbuf, gbuf);

                        SendNum(dist);
                       
                        delay_ms(500); // 稍作延时
                }
        }
   

}

科学妞妞 发表于 2026-3-21 07:07:59

nekdyldm 发表于 2026-3-20 09:26
#include
#include



你的程序应该不是用在STc32G芯片上的,程序移植需要配合你用的芯片做适应性修改。如果,用STC32G系列的或者Ai8051U用32位C251编译,原来提供的程序应该无障碍。

nekdyldm 发表于 2026-3-23 10:14:17

科学妞妞 发表于 2026-3-21 07:07
你的程序应该不是用在STc32G芯片上的,程序移植需要配合你用的芯片做适应性修改。如果,用STC32G系列的或 ...

芯片是stc15w408as

页: [1] 2
查看完整版本: 【分享】VL53L0X激光测距 (STC32G) + ST7735显示 完整代码与分析