串口屏多档位舵机控制 — 从 main 函数开始的完整代码解读
作者:flor与openclaw+opencode协作完成
日期:2026-05-28
项目:串口屏 + 舵机,触摸按钮控制四档往复扫动
MCU:STC8A8K64D4 @ 24MHz | 屏幕:迪文 DGUS T5L | 舵机:SG90(外部 5V 供电)
工程路径:功能实现/
当 CPU 上电复位后,STARTUP.A51 最先执行——它清零内部 RAM、初始化堆栈指针,最后 LJMP ?C_START 跳转到 C 运行环境,进入 main()。
这就是我们故事的起点。
目录
- 工程概览
- config.h — main() 看到的第一份蓝图
- main() 初始化 — 六步让硬件就绪
- UART2 数据接收 — 中断与主循环的配合
- 帧解析状态机 — 从字节流中提取完整帧
- 舵机扫动循环 — 往复运动的软件实现
- dgus.c — 屏幕通信四函数
- 完整执行流 — 从触摸到舵机响应
- 附录
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)); // 更新舵机位置
}
逻辑流程:
- 查
max_angle[gear_mode]获取当前档位目标角度 - 0 档直接停在 0°,返回
- 非 0 档时,根据
sweep_up标志决定方向:正向则cur_angle += 1,反向则cur_angle -= 1 - 到达边界(≥target 或 ≤0)时翻转
sweep_up标志 - 调用
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/