浮云落笔

浮云落笔

首页
瞬间
反馈
浮云落笔

浮云落笔

首页 瞬间 反馈
  1. 首页
  2. 8051
  3. 串口屏多档位舵机控制

串口屏多档位舵机控制

  • 8051
  • 发布于 2026-05-28
  • 14 次阅读
flor
flor

串口屏多档位舵机控制 — 从 main 函数开始的完整代码解读

作者:flor与openclaw+opencode协作完成
日期:2026-05-28
项目:串口屏 + 舵机,触摸按钮控制四档往复扫动
MCU:STC8A8K64D4 @ 24MHz | 屏幕:迪文 DGUS T5L | 舵机:SG90(外部 5V 供电)
工程路径:功能实现/


当 CPU 上电复位后,STARTUP.A51 最先执行——它清零内部 RAM、初始化堆栈指针,最后 LJMP ?C_START 跳转到 C 运行环境,进入 main()。

这就是我们故事的起点。


目录

  1. 工程概览
  2. config.h — main() 看到的第一份蓝图
  3. main() 初始化 — 六步让硬件就绪
  4. UART2 数据接收 — 中断与主循环的配合
  5. 帧解析状态机 — 从字节流中提取完整帧
  6. 舵机扫动循环 — 往复运动的软件实现
  7. dgus.c — 屏幕通信四函数
  8. 完整执行流 — 从触摸到舵机响应
  9. 附录

1. 工程概览

功能实现/
├── main.c          ★ 业务主程序:初始化 + 帧解析 + 舵机扫动         (3435 B)
├── dgus.c          ★ DGUS 通信模块:档位切换 + 写变量 + LED 控制   (1347 B)
├── dgus.h          模块接口声明                                    (265 B)
├── config.h        全局配置:主频、外设头文件、类型宏               (2550 B)
├── isr.c           中断向量表(21 个中断入口,UART2 中断转发)      (9793 B)
├── isr.h           中断处理函数 extern 声明                         (1208 B)
├── inisr.asm       扩展中断向量跳转表                              (2217 B)
├── STARTUP.A51     Keil 启动文件                                   (6376 B)
├── STC8.uvproj     Keil 工程描述 (XML, 含编译选项和文件分组)        (17793 B)
│
└── ESD-库/         底层驱动库(.obj 预编译)
    ├── GPIO.h/obj  GPIO 初始化和引脚操作
    ├── UART.h/obj  串口初始化和字节发送
    ├── PWM.h/obj   PWM 初始化和占空比更新
    └── ...         其他模块(delay, ADC, EEPROM 等)

编译配置(来自 STC8.uvproj):

项 值
器件 STC8A8K64D4 Series
主频 35MHz(编译设置)/ 24MHz(代码中实际使用)
存储模型 Large(变量默认在 XDATA)
优化级别 8(最高,侧重速度)
链接选项 REMOVEUNUSED(移除未用代码段)

Keil 工程文件分组:

  • User 组:STARTUP.A51, main.c, isr.c, inisr.asm — 用户代码
  • ESD 组:GPIO, delay, PWM, UART 等 12 个库模块 — 外设驱动
  • dgus 组:dgus.c, dgus.h — 屏幕通信

2. config.h — main() 看到的第一份蓝图

main.c 的第一行:

#include "config.h"

编译时 config.h 先展开,铺好整个工程的基础设施。

2.1 主频选择

#define MAIN_Fosc  24000000L    // 主频 24MHz

STC8A8K64D4 最高可跑 35MHz,这里稳定选用 24MHz。这个值被 PWM、UART、delay 等所有外设库引用——改这一个数字,所有需要时钟计算的地方自动适配。

附带几个便捷宏:

#define Main_Fosc2   (MAIN_Fosc / 2)      // 12MHz
#define Main_Fosc_KHZ (MAIN_Fosc / 1000)   // 24000 KHz
#define Main_Fosc_MHZ (MAIN_Fosc / 1000000) // 24 MHz

2.2 类型别名

#define uchar  unsigned char       // 8 位,  范围 0~255
#define uint   unsigned int        // 16 位, 范围 0~65535
#define u16    unsigned int
#define u32    unsigned long       // 32 位, 范围 0~4294967295

8051 中 int 是 16 位的。这些别名让变量声明更紧凑。

2.3 外设模块引用

#include "GPIO.h"    // GPIO_init_allpin, GPIO_init_pin, Out_IO, Get_IO
#include "PWM.h"     // PWM_init, PWM_change
#include "UART.h"    // UART_init, UART_Send_byte, UART_Send_string
#include "delay.h"   // delay_ms, delay_us
#include "ADC.h"
#include "EEPROM.h"
// ... 其他模块

每个 .h 文件声明了对应 .obj 预编译库中的函数接口。链接时 Keil 找到这些符号,绑在一起生成最终的 .hex 固件。


3. main() 初始化 — 六步让硬件就绪

void main(void) {
    // ========== 第 1 步:GPIO ==========
    GPIO_init_allpin(0);       // 所有 IO 设为准双向模式
    GPIO_init_pin(20, 1);      // P20 单独设为推挽输出

    // ========== 第 2 步:串口 ==========
    UART_init(1, 115200, 1, 0);  // UART1: 调试口, 不开启接收中断
    UART_init(2, 115200, 1, 1);  // UART2: DGUS 屏通信口, 开启接收中断

    // ========== 第 3 步:PWM ==========
    PWM_init(20, 50, 250);     // P20, 50Hz, 初始占空比 250

    // ========== 第 4 步:中断总开关 ==========
    EA = 1;

    // ========== 第 5、6 步:主循环 ==========
    while(1) {
        // 帧解析 + 舵机扫动
    }
}

下面逐行解释每一步"做了什么"和"为什么这么做"。

第 1 步:GPIO_init_allpin(0) + GPIO_init_pin(20, 1)

GPIO_init_allpin(0)  →  所有引脚设为准双向模式

准双向模式是 8051 的经典 IO 模式——输出高电平时靠内部弱上拉电阻(约 50KΩ),驱动电流不到 1mA;输出低电平时主动拉低,驱动能力较强。适合大多数数字 IO 场景。

GPIO_init_pin(20, 1)  →  P20 单独设为推挽输出 (模式=1)

为什么要单独改 P20? 因为 P20 要输出 PWM 信号驱动舵机。准双向模式的弱上拉会让 PWM 波形的上升沿变得缓慢(RC 充电效应),导致舵机抖动或无法准确响应。推挽模式用一对 MOS 管主动推拉电平,波形干净利落。

GPIO_init_pin() 的参数:20 表示 P2.0 引脚,1 表示推挽输出模式。

第 2 步:UART_init() 两个串口

UART_init(串口号, 波特率, 定时器, 是否开启中断)
         (1,     115200,   1,      0)     UART1: P30/P31, 调试用, 无中断
         (2,     115200,   1,      1)     UART2: P10/P11, DG通信用, 开中断

UART1(调试口):连接 PC 的 USB 转串口模块。在主循环中收到 DGUS 帧后原样转发到 UART1,PC 端串口助手可以看到数据内容,用于调试。不需要接收数据,所以不开启接收中断。

UART2(DGUS 屏通信口):连接串口屏。需要实时接收屏幕发来的触摸返回帧,所以第四个参数为 1——开启接收中断,每收到一个字节就触发 UART2_I() interrupt 8。

第 3 步:PWM_init(20, 50, 250)

PWM_init(引脚, 频率Hz, 初始占空比)
        (P20,    50,      250)

SG90 舵机需要 50Hz PWM(周期 20ms)。STC8 的 PWM 模块在 50Hz 下,占空比取值范围 0-10000 对应 0%-100%:

占空比 duty 脉宽 占空比% 舵机角度
250 0.5ms 2.5% 0°
500 1.0ms 5.0% 45°
750 1.5ms 7.5% 90°
1000 2.0ms 10.0% 135°
1250 2.5ms 12.5% 180°

这是线性关系:每 1° 对应 duty 变化 (1250-250)/180 = 1000/180 ≈ 5.556。

初始占空比设为 250,舵机上电即停在 0°,等待指令。

第 4 步:EA = 1

EA = 1;   // 总中断使能

必须放在初始化的最后。如果反了——先开中断再初始化 UART——可能在 UART 初始化中途就触发中断,此时 UART 寄存器还是乱的,后果不可控。


4. UART2 数据接收 — 中断与主循环的配合

4.1 中断层:isr.c → main.c

串口屏发送的数据以字节为单位到达 STC 的 P10 (RXD2) 引脚。硬件自动将字节存入 S2BUF 寄存器,并置起 RI 中断标志。

在 isr.c 中,UART2 中断入口:

void UART2_I() interrupt 8 {          // 中断号 8 = UART2
    if (S2CON & 0x01) {               // 检查 RI(接收完成标志)
        UART2_isr();                  // 调用 main.c 中的处理函数
        S2CON &= ~0x01;               // 清除 RI 标志
    }
}

为什么分两层? Keil C51 要求 interrupt N 函数有特殊的序言/尾声代码(保存恢复寄存器、使用指定寄存器组)。如果在 main.c 里写 interrupt 8,会影响整个文件的编译方式。分层设计让 isr.c 专管硬件中断入口,main.c 写普通 C 函数,互不干扰。

4.2 消费层:UART2_isr()

void UART2_isr(void) {
    UART_DATA[next] = S2BUF;   // 从硬件寄存器读字节,写入环形缓冲
    next++;                     // 生产指针后移
}

就两行,极致精简。next 是 uchar 类型(0~255),加到 256 时自动溢出回绕为 0,天然实现了 256 字节的环形缓冲——不需要 next % 256。

4.3 环形缓冲的生产-消费模型

               ┌──────────────────────────┐
               │  UART_DATA[256]          │
  UART2 ISR ──→│  next 写入 (生产者)       │──→ 主循环
               │  current 读取 (消费者)    │    读取
               └──────────────────────────┘

这是一个单生产者单消费者的环形缓冲:

  • 只有 ISR 写 next,只有 主循环读 current——没有竞争
  • 不需要关中断、不需要互斥锁
  • next != current 意味着有未处理数据
if(next != current) {    // 有数据?
    // 消费一个字节……
    current++;            // 消费后指针后移
}

5. 帧解析状态机 — 从字节流中提取完整帧

串口传输的是连续的字节流,没有天然的"帧边界"。状态机要做的是:在这片字节流中找到帧头、确定帧长度、收齐完整帧。

5.1 DGUS 帧格式

屏幕收发的数据帧统一遵循这个结构:

5A A5 [长度N] [数据1] [数据2] ... [数据N]
│  │     │       └──────── N 字节数据 ────────┘
│  │     └── 数据部分长度(不含帧头和长度自身)
│  └── 帧头第二字节(固定 0xA5)
└── 帧头第一字节(固定 0x5A)

完整帧总字节数 = N + 3(帧头 2 字节 + 长度 1 字节 + 数据 N 字节)。

5.2 四状态转换

           找到5A?            确认A5?            取长度N            收N字节?
              │                  │                  │                  │
   ┌──────────▼──────┐  ┌────────▼────────┐  ┌─────▼──────┐  ┌───────▼────────┐
   │  case 0         │  │  case 1         │  │  case 2    │  │  case 3        │
   │  寻找帧头 0x5A  │──│  确认帧头 0xA5   │──│  读长度字节  │──│  逐字节收数据   │
   │                 │  │                 │  │  ja = N+1  │  │  ja-- 直到 ja=1 │
   └─────────────────┘  └─────────────────┘  └────────────┘  └───────┬────────┘
                                                                      │ ja==1
                                                          ┌───────────▼──────────┐
                                                          │ 帧收齐!              │
                                                          │ UART_1(1, frame)  回显│
                                                          │ gears(frame)      切档│
                                                          │ led01(frame)      亮灯│
                                                          │ flag=0, ia=0     复位 │
                                                          └──────────────────────┘

状态 0:寻找 0x5A

case 0: {
    if(UART_DATA[current] == 0x5a) {
        uart2_data[ia] = UART_DATA[current];  // ia=0,写入帧头第一个字节
        ia++;                                  // ia=1
        UART_FLAG = 1;                         // → 状态 1
    }
    // 不是 0x5A → 丢弃该字节,保持状态 0 继续找
} break;

状态 1:确认 0xA5

case 1: {
    if(UART_DATA[current] == 0xa5) {
        uart2_data[ia] = UART_DATA[current];  // ia=1,写入帧头第二个字节
        ia++;                                  // ia=2
        UART_FLAG = 2;                         // → 状态 2
    }
    // 不是 0xA5 → 保持状态 1,等下一个字节(不会误判,因为 DGUS 帧 5A 后必跟 A5)
} break;

状态 2:读取长度

case 2: {
    if(ia == 2) {                              // 加守卫:只在第一次进入状态 2 时执行
        uart2_data[ia] = UART_DATA[current];   // ia=2,写入长度字节
        ia++;                                   // ia=3
        ja = uart2_data[2] + 1;                // 倒数计数器 = N+1
        UART_FLAG = 3;                          // → 状态 3
    }
} break;

ia == 2 是一个守卫条件——状态 2 可能被执行多次(如果状态 1 之后立刻又收到字节)。ia 只在首次置为 2 然后递增为 3,后续进入状态 2 时 ia 已经是 3,条件不成立,不会重复读取。

ja = uart2_data[2] + 1:uart2_data[2] 是帧长度 N。N 字节数据加 1 作为倒数计数器初始值——在状态 3 中每收一字节 ja--,当 ja == 1 时意味着收到了 N 字节数据,帧完整。

状态 3:逐字节收数据

case 3: {
    ja--;                                      // 倒数计数器递减
    uart2_data[ia] = UART_DATA[current];       // 写入当前字节
    ia++;                                      // 缓冲指针后移

    if(ja == 1) {                              // 倒数到 1 → 完整帧收齐
        ia = 0;                                // 帧缓冲指针复位
        UART_1(1, uart2_data);                 // ┐
        gears(uart2_data);                     // ├ 三合一处理
        led01(uart2_data);                     // ┘
        UART_FLAG = 0;                         // 回到状态 0
    }
} break;

帧收齐后调用三个函数,然后 UART_FLAG = 0 回到原点,准备寻找下一个帧。

ia = 0 复位帧缓冲写入指针,下一个帧的数据从头开始覆盖 uart2_data[]。


6. 舵机扫动循环 — 往复运动的软件实现

UART 帧处理只在 if(next != current) 里执行。而下面的舵机控制每个循环周期都执行:

while(1) {
    // ... 帧解析 ...

    sweep_timer++;
    if(sweep_timer >= 9000) {
        sweep_timer = 0;
        servo_sweep();
    }
}

6.1 软件定时器的原理

24MHz 下,主循环空转一圈大约几十微秒。sweep_timer 每圈加 1,累积到 9000 大约耗时 300~500ms,触发一次舵机步进。

这是一个软件定时器——不占用硬件定时器资源的节拍发生器。改变阈值即可调节扫动速度:阈值越小越快,越大越慢。

6.2 四档参数

uchar gear_mode = 0;                    // 当前档位 (0=停机, 1/2/3=扫动)
int  max_angle[4] = {0, 60, 120, 180}; // 每档最大扫动角度
档位 最大角度 扫动范围 效果
0 0° 不动 停机
1 60° 0° ↔ 60° 小幅往复
2 120° 0° ↔ 120° 中幅往复
3 180° 0° ↔ 180° 大幅往复

6.3 servo_sweep() — 扫动算法

void servo_sweep(void) {
    uint step = 1;                       // 每次步进 1°
    uint target = max_angle[gear_mode];  // 查表获取当前档目标

    if(gear_mode == 0) {                 // 停机档
        cur_angle = 0;
        PWM_change(20, 250);             // 舵机归 0°
        return;                          // 直接返回,不扫动
    }

    if(sweep_up) {                       // 正向:角度增大
        cur_angle += step;
        if(cur_angle >= target) {        // 到达上限
            cur_angle = target;
            sweep_up = 0;                // 翻转方向
        }
    } else {                             // 反向:角度减小
        if(cur_angle <= step) {          // 到达下限
            cur_angle = 0;
            sweep_up = 1;                // 翻转方向
        } else {
            cur_angle -= step;
        }
    }

    PWM_change(20, angle_to_duty(cur_angle));  // 更新舵机位置
}

逻辑流程:

  1. 查 max_angle[gear_mode] 获取当前档位目标角度
  2. 0 档直接停在 0°,返回
  3. 非 0 档时,根据 sweep_up 标志决定方向:正向则 cur_angle += 1,反向则 cur_angle -= 1
  4. 到达边界(≥target 或 ≤0)时翻转 sweep_up 标志
  5. 调用 PWM_change() 更新舵机位置

step = 1 意味着每次调用只改变 1°。60° 的目标需要 60 次调用(约 18~30 秒)才能扫完。如果改成 step = 3,速度就是原来的 3 倍。step 和 sweep_timer 阈值配合,可以独立调节速度和平滑度。

6.4 angle_to_duty() — 角度到占空比

uint angle_to_duty(uint angle) {
    return (uint)(250 + ((unsigned long)angle * 1000 / 180));
}

推导过程:SG90 在 50Hz PWM 下,0° 对应 duty = 250,180° 对应 duty = 1250。线性关系:

duty = 250 + angle × (1250 - 250) / 180
     = 250 + angle × 1000 / 180

(unsigned long) 强制转换是关键:angle * 1000 在 angle = 180 时等于 180000,超过 uint 的表示范围 65535。先提升到 32 位(unsigned long)计算,再截断回 16 位,避免溢出。


7. dgus.c — 屏幕通信四函数

dgus.c 是独立模块,封装了所有与串口屏交互的功能。main.c 通过 #include "dgus.h" 引用。

7.1 gears() — 档位切换

整个系统最核心的业务函数:

uchar gears(uchar* pp) {
    uchar i;
    i = pp[2];                          // 帧长度 N

    if(pp[i+2] == 0x05) {              // 帧尾字节 = 0x05 → 升档
        if(gear_mode < 3) {
            gear_mode++;                // 1→2, 2→3
        }
        dgus_write_vp(0x2000, gear_mode);  // 同步屏幕显示
    }
    else if(pp[i+2] == 0x04) {         // 帧尾字节 = 0x04 → 降档
        if(gear_mode > 0) {
            gear_mode--;                // 3→2, 2→1, 1→0
        }
        if(gear_mode <= 0) gear_mode = 0;   // 防下溢
        dgus_write_vp(0x2000, gear_mode);
    }
    return 0;
}

帧尾取字节技巧:

pp[2] = N (帧长度)
pp[N+2] = 帧头(2字节) + 长度(1字节) 之后的第 N 字节 = 帧的最后一字节

例如 N=6:
  索引: 0   1   2   3   4   5   6   7   8
  内容: 5A  A5  06  83  20  01  01  00  05
  pp[2]=6, pp[6+2]=pp[8]=0x05 = 键值低字节(升档)

这种解析方式不关心 DGUS 帧内部字段(命令、变量地址、键值位置)的具体偏移量,直接读最后一字节做判断。如果 DGUS Tool 中改了变量地址或键值参数,C 代码不用改。

边界保护:

  • 升档 if(gear_mode < 3) → 最多 3 档
  • 降档 if(gear_mode > 0) + 兜底 if(gear_mode <= 0) gear_mode = 0

同步屏幕:dgus_write_vp(0x2000, gear_mode) 向屏幕的变量地址 0x2000 写入当前档位值。屏幕的"变量图标"控件根据这个值自动切换显示的图标。

7.2 dgus_write_vp() — 向屏幕写变量

void dgus_write_vp(uint addr, uint val) {
    uchar cmd[8];
    cmd[0] = 0x5A;                     // 帧头 1
    cmd[1] = 0xA5;                     // 帧头 2
    cmd[2] = 0x05;                     // 数据长度:5 字节
    cmd[3] = 0x82;                     // 命令:写变量寄存器
    cmd[4] = (addr >> 8) & 0xFF;       // 变量地址高字节
    cmd[5] = addr & 0xFF;              // 变量地址低字节
    cmd[6] = (val >> 8) & 0xFF;        // 数据高字节
    cmd[7] = val & 0xFF;               // 数据低字节
    UART_1(2, cmd);                    // UART2 发送
}

按照 DGUS "写变量寄存器" 帧格式拼装 8 字节命令:

5A A5 05 82 [addr_H] [addr_L] [val_H] [val_L]

(addr >> 8) & 0xFF 显式加了 & 0xFF 掩码——某些 8051 编译器对 uint 移位的高位处理在不同优化级别下不一致,显式掩码确保行为确定。

7.3 led01() — 辅助 LED

uchar led01(uchar* pp) {
    uchar i;
    i = pp[2];                         // 帧长度
    if(pp[i+2] == 0x00) {
        Out_IO(01, 0);                 // P01 低电平 → LED 灭
    } else if(pp[i+2] == 0x01) {
        Out_IO(01, 1);                 // P01 高电平 → LED 亮
    }
    return 0;
}

同样用帧尾字节技巧,根据最后一字节的值控制 P01 引脚。Out_IO(01, 0) 中第一个参数 01 是 IO 组号——01 = P0 口第 1 位(即 P01),仅验证状态机功能。

7.4 UART_1() — 通用帧发送

void UART_1(unsigned char pin, unsigned char *pt) {
    uint i;
    uint len = pt[2] + 3;              // N + 3 = 完整帧总字节数
    for(i = 0; i < len; i++) {
        UART_Send_byte(pin, *pt++);
    }
}

len 用 uint 而非 uchar:如果 N = 254,则 254 + 3 = 257。uchar 只能表示 0~255,257 会溢出截断为 1。uint 能正确处理这情况。

pin 参数指定串口号(1 = UART1 调试,2 = UART2 屏幕)。


8. 完整执行流 — 从触摸到舵机响应

8.1 上电启动

 上电复位
   │
   ▼
 STARTUP.A51  →  清零内部 RAM (IDATA区域)  →  初始化堆栈 SP  →  LJMP main()
   │
   ▼
 main() 初始化:
   GPIO:   全 IO 准双向  →  P20 推挽
   UART:   UART1(调试) + UART2(DGUS,中断)
   PWM:    P20, 50Hz, duty=250 (0°)
   EA=1    ← 中断总开 (放在最后!)
   │
   ▼
 while(1) 主循环开始

8.2 触摸 [+] 按钮,从 0 档升到 1 档

 手指触摸屏幕 [+] 按钮
   │
   ▼
 DGUS 通过 UART2 TX 逐字节发送触摸返回帧:
   5A A5 06 83 20 01 01 00 05   (9 字节, 最后一字节=0x05)
            └────┬────┘ └──┬──┘
            变量地址0x2001  键值0x0005
   │
   ▼ (逐字节到达 STC P10)
 UART2 硬件 → S2BUF → RI=1 → 触发中断
   │
   ▼
 isr.c: UART2_I() interrupt 8
   └── UART2_isr()  →  UART_DATA[next++] = S2BUF
   │
   ▼ (中断返回)
 main() while(1) 帧解析:
   next != current → 有数据!
   case 0: 找到 0x5A ✓ → flag=1
   case 1: 确认 0xA5 ✓ → flag=2
   case 2: ia==2, 长度=06 ✓ → ja=7, flag=3
   case 3: 逐字节接收 83→20→01→01→00→05
           ja: 7→6→5→4→3→2→1 → ja==1! 帧收齐
           ├─ UART_1(1, frame)   → PC 串口助手看到完整帧
           ├─ gears(frame)       → 帧尾=0x05
           │   gear_mode: 0→1
           │   └─ dgus_write_vp(0x2000, 1) → 屏幕显示 1 档图标
           └─ led01(frame)       → P01=1, LED 亮
   │
   ▼
 舵机扫动开始 (gear_mode=1):
   sweep_timer: 1→2→...→8999→0 → servo_sweep():
     target = max_angle[1] = 60°
     cur_angle: 0→1→2→...→60→59→...→0→1→...
     PWM_change(20, angle_to_duty(cur_angle))
   │
   ▼
 SG90 舵机在 0° ↔ 60° 之间来回摆动

8.3 连续触摸,切到 3 档再降到 0 档

 触摸 [+]       → gears(): 帧尾=0x05 → gear_mode: 1→2
    target = max_angle[2] = 120°  → 扫动范围扩大到 0°↔120°

 触摸 [+]       → gears(): 帧尾=0x05 → gear_mode: 2→3
    target = max_angle[3] = 180°  → 扫动范围扩大到 0°↔180°

 触摸 [-]       → gears(): 帧尾=0x04 → gear_mode: 3→2
    target = 120°  → 范围缩小

 触摸 [-]       → gear_mode: 2→1 → target=60°

 触摸 [-]       → gear_mode: 1→0
    servo_sweep(): gear_mode==0 → cur_angle=0, PWM_change(20,250)
    舵机停转

8.4 主循环的并发时序

while(1) 每圈做的事情:

  ┌─ 检查 UART → 如果有数据,执行状态机一步(一字节)
  │     ↓ (无数据时跳过)
  ├─ sweep_timer++
  └─ if(sweep_timer >= 9000) → servo_sweep()

  两个任务在同一循环中交替,互不阻塞:
  即使舵机在扫动中,收到触摸帧也能立即响应
  即使正在处理帧,舵机扫动计时也不会停止

9. 附录

A. 硬件接线

DGUS T5L 串口屏            STC8A8K64D4             SG90 舵机
┌──────────┐             ┌──────────────┐        ┌──────────┐
│ TXD2     │────→────────│ P10 (RXD2)   │        │          │
│ RXD2     │←────────────│ P11 (TXD2)   │        │          │
│ GND      │──────┬──────│ GND          │──┬─────│ 棕 GND   │
└──────────┘      │      └──────────────┘  │     └──────────┘
                  │                        │
                  │             P20 ───────┼──→ 橙 SIG
                  │                        │
                  │             外部 5V ───┼──→ 红 VCC
                  │                        │
                  └────────────────────────┘ (共地)
  • 串口参数:UART2, 115200 bps, 8N1
  • 舵机供电:必须外部独立 5V(堵转电流可达 700mA+,STC 板载 LDO 带不动)

B. DGUS 屏幕控件配置

控件 变量地址 (VP) 参数 功能
变量图标 0x2000 4 张图标 (值 0/1/2/3) 显示当前档位
按键返回 0x2001 [-] 键值 0x0004 发送降档帧
按键返回 0x2001 [+] 键值 0x0005 发送升档帧

C. DGUS 协议帧格式

触摸返回帧(屏 → MCU):

5A A5 06 83 VP_H VP_L 01 KEY_H KEY_L

写变量帧(MCU → 屏):

5A A5 05 82 VP_H VP_L VAL_H VAL_L

两种帧的帧头(5A A5)、长度字段、命令字段位置完全一致,状态机能统一解析。

D. 关键全局变量

变量 类型 位置 用途
gear_mode uchar main.c 定义, dgus.h extern 当前档位,gears() 写, servo_sweep() 读
max_angle[4] int[] main.c 各档目标角度
cur_angle uint main.c 舵机当前角度 (0~180)
sweep_up bit main.c 扫动方向标志
sweep_timer uint main.c 软件节拍计数器
UART_DATA[256] uchar[] main.c 环形接收缓冲
UART_FLAG uchar main.c 帧解析状态 (0~3)

E. 中断系统架构

inisr.asm ──→ P0~P9 INT 扩展中断 → 跳转到 isr.c 的 GPIO_I() (interrupt 25)

isr.c ──→ 21 个中断入口函数
  ├─ UART2_I() interrupt 8   →  UART2_isr() (main.c)   ★ 核心
  ├─ UART1_I() interrupt 4   →  只清标志 (调试口不发不收)
  ├─ PCA_I()  interrupt 7    →  模式分发 (预留, 业务函数未启用)
  └─ 其他 18 个               →  清标志 / 空壳 (保持中断向量表完整)

F. 编译和烧录

  • 编译输出:HEX/STC8_HEX.hex (18554 bytes)
  • 烧录工具:STC-ISP (通过 USB 转串口连接 STC 的 UART1)
  • 包含模块:GPIO, UART, PWM, delay 等 12 个底层库 + User 代码

工程路径:
Windows Keil:Desktop/stc8a8k64d4/Project/功能实现/
Linux 备份:/home/ctyun/.openclaw/workspace/

目录
湘ICP备2025147565号-1
gongan beian 湘公网安备43102602000213号