为什么是 DOS,而不是 Linux 或单片机
在做线切割机(Wire EDM)这类运动控制设备的控制系统选型时,通常有三条路:
| 方案 |
优势 |
劣势 |
| 单片机(STM32等) |
成本低、可靠、专用 |
算力有限,复杂插补算法(三次样条、NURBS)跑不动 |
| Linux + 实时补丁(PREEMPT_RT) |
现代工具链、算力强 |
调度延迟不确定,中断响应受内核调度影响,工程复杂 |
| DOS(裸机/近裸机) |
没有调度器抢占问题,中断响应可预测,x86 算力够用 |
工具链老旧,需要自己管理硬件 |
DOS 本身没有真正意义上的多任务调度——它是单任务操作系统,你的程序独占 CPU。这恰恰是运动控制场景需要的:你自己完全掌控中断优先级和响应时序,不会被无关的系统进程抢占。这也是为什么很多工业设备(老式数控机床、老式医疗设备)至今还在跑 DOS 或 DOS 衍生的实时系统。
TSR(Terminate and Stay Resident,终止并驻留)技术,就是在 DOS 环境下实现"后台常驻服务"的核心机制——对于线切割机来说,它就是实时插补计算循环和伺服脉冲发生器的载体。
第一部分:TSR 的基本原理
1.1 什么是 TSR
正常的 DOS 程序执行完毕后,会调用 INT 21h AH=4Ch(正常终止)把内存还给系统。而 TSR 程序执行"终止"操作后,依然占用内存,并且已经把自己的代码挂接到某个中断向量上——之后每次那个中断触发,都会执行 TSR 里的代码,即使原程序早已"退出"。
1
2
3
4
5
|
普通程序: 加载 → 执行 → 终止 → 释放全部内存
TSR 程序: 加载 → 初始化 → 挂接中断 → "终止但驻留"
↓
内存里的代码持续通过中断被调用
|
1.2 两种终止并驻留的方式
| 中断调用 |
说明 |
| INT 27h(旧式,DOS 1.0起) |
简单但灵活性差,驻留大小限制在 64KB 内 |
| INT 21h AH=31h(推荐,DOS 2.0+) |
现代 TSR 标准做法,可以设置任意驻留大小,还能返回错误码 |
1
2
3
4
5
6
7
8
9
10
|
#include <dos.h>
void keep_resident(unsigned int paragraphs, unsigned char exit_code) {
union REGS regs;
regs.h.ah = 0x31; // Keep Process (TSR)
regs.h.al = exit_code; // 退出码
regs.x.dx = paragraphs; // 驻留内存大小(以16字节为单位)
int86(0x21, ®s, ®s);
// 此调用不会返回
}
|
内存大小的计算:驻留内存以"段"(paragraph,16字节)为单位。如果你的常驻代码+数据总共 4KB,需要设置:
1
|
paragraphs = (4096 + 15) / 16; // 向上取整
|
第二部分:中断向量表与劫持机制
2.1 中断向量表(IVT)结构
实模式下,中断向量表位于内存最低地址 0000:0000,共 256 个中断(每个 4 字节:2字节偏移 + 2字节段地址):
1
2
3
4
5
6
7
8
9
|
地址 中断号 用途
0000:0000 INT 00h 除零错误
0000:0004 INT 01h 单步调试
...
0000:0020 INT 08h 系统定时器(18.2 次/秒,关键!)
...
0000:0024 INT 09h 键盘中断
...
0000:00CC INT 33h 鼠标服务
|
2.2 中断劫持的标准流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <dos.h>
void interrupt (*old_timer_handler)();
void interrupt new_timer_handler() {
// === 你的实时任务代码 ===
do_interpolation_step(); // 插补计算的一步
output_pulse(); // 输出伺服脉冲
// 调用原处理程序(维持系统时钟正常)
(*old_timer_handler)();
}
void install_tsr() {
old_timer_handler = getvect(0x08); // 保存原向量
setvect(0x08, new_timer_handler); // 挂接新向量
}
void uninstall_tsr() {
setvect(0x08, old_timer_handler); // 恢复原向量(退出前必须!)
}
|
关键细节:INT 08h 是系统定时器中断,来自 8253/8254 PIT 芯片,默认频率 18.2 Hz(每 54.925 毫秒一次)。DOS 的时钟计数、delay() 函数、很多"假多任务"效果全都基于这个中断。
第三部分:面向运动控制的定时器重编程
3.1 提升定时器频率
18.2 Hz 对于插补计算和脉冲输出来说太慢了(约每 55ms 一次,而伺服脉冲通常需要几 kHz 到几十 kHz)。需要重新编程 8253/8254 PIT 芯片,提高中断频率。
PIT 芯片的输入时钟是 1.193182 MHz,中断频率由分频值决定:
例如要达到 10kHz 中断频率:
1
2
3
4
5
6
7
8
9
|
#include <dos.h>
void set_timer_frequency(unsigned int freq_hz) {
unsigned int divisor = 1193182L / freq_hz;
outportb(0x43, 0x36); // 控制字:通道0,先写低字节后高字节,模式3
outportb(0x40, divisor & 0xFF); // 分频值低字节
outportb(0x40, (divisor >> 8) & 0xFF); // 分频值高字节
}
|
极其重要的一点:改变了 PIT 频率后,DOS 内部依赖 18.2Hz 的逻辑(时钟、delay())都会变快或变慢。所以中断处理函数里通常需要手动模拟原始的 18.2Hz 节拍,累积计数到一定值才调用原处理程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#include <dos.h>
#define TICK_RATIO (10000 / 18) // 新频率相对于18.2Hz的倍数(近似)
void interrupt (*old_timer_handler)();
volatile unsigned long tick_counter = 0;
volatile long accumulator = 0;
void interrupt new_timer_handler() {
// === 高频实时任务(每次都执行)===
do_interpolation_step();
output_servo_pulse();
tick_counter++;
// === 低频任务:模拟原始18.2Hz节拍,维持DOS时钟正常 ===
accumulator += 18;
if (accumulator >= 10000) {
accumulator -= 10000;
(*old_timer_handler)(); // 触发原BIOS时钟处理
} else {
outportb(0x20, 0x20); // 手动向 8259 PIC 发送 EOI(中断结束)信号
}
}
|
为什么需要手动发 EOI? 因为原来的 BIOS 时钟处理程序里会发 EOI(End of Interrupt)信号给 8259 中断控制器,告诉它"这次中断处理完了,可以接收下一次"。如果你跳过了原处理程序,就必须自己发这个信号,否则同级或低优先级中断会被永久阻塞。
1
2
|
8259 PIC 端口 0x20:主控制器命令端口
写入 0x20 = EOI(非特定结束)命令
|
第四部分:中断服务程序(ISR)的编程约束
4.1 中断处理函数中的禁忌
在中断上下文里执行的代码,跟普通函数完全不同的规则:
| 禁止/受限操作 |
原因 |
| 调用 DOS 服务(INT 21h) |
DOS 本身不可重入!如果中断发生时 DOS 正在执行别的调用,再次进入会破坏内部状态 |
| 动态内存分配(malloc) |
DOS 的内存管理函数不可重入,同上 |
| printf/scanf 等标准库函数 |
内部大量调用 DOS 服务 |
| 长时间循环 |
会阻塞后续中断,影响系统响应 |
| 浮点运算(部分情况) |
需要保存/恢复 FPU 状态,否则会污染主程序的浮点上下文 |
4.2 DOS 不可重入问题的标准解法
工业级 TSR 采用"延迟处理“模式——中断服务程序只做最紧急、最快的操作(读硬件、置标志位),复杂逻辑放到主循环里处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
volatile int pulse_request_flag = 0;
volatile long position_x = 0, position_y = 0;
void interrupt new_timer_handler() {
// 中断内:只做最简单的硬件操作
if (need_pulse()) {
outportb(PULSE_PORT, PULSE_HIGH);
pulse_request_flag = 1; // 标记,交给主循环处理
}
outportb(0x20, 0x20); // EOI
}
int main() {
install_tsr();
while (1) {
if (pulse_request_flag) {
pulse_request_flag = 0;
// 复杂的插补计算、DOS文件I/O、屏幕显示等都放这里
calculate_next_interpolation_point();
update_display();
}
}
}
|
这个模式本质上就是中断只负责"时间关键"部分,主循环负责"逻辑复杂"部分——跟现代嵌入式开发里 ISR + 任务队列的思想完全一致。
4.3 关中断保护临界区
当主循环和中断处理程序共享数据(如上面的 position_x),如果数据超过一个字(16位),读写不是原子操作,可能被中断打断导致数据损坏。这时需要临时关闭中断:
1
2
3
4
5
6
7
8
9
|
#include <dos.h>
long read_position_safely() {
long result;
disable(); // CLI 指令,关中断
result = position_x; // 32位读取,非原子操作,需要保护
enable(); // STI 指令,开中断
return result;
}
|
关中断的代价:关中断期间,所有中断(包括定时器)都会被阻塞。对于运动控制场景,关中断时间必须做到微秒级,否则会导致插补计算错过节拍、脉冲输出抖动。
第五部分:TSR 与栈的问题
5.1 为什么需要独立的中断栈
DOS 中断发生时,会复用当前正在执行的程序的栈。如果主程序的栈本身已经很满(深层函数调用),中断处理程序再往上叠加局部变量,可能导致栈溢出,这是 DOS TSR 最常见也最难调的 bug 之一。
工业级 TSR 的标准做法是切换到独立的中断栈:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#define ISR_STACK_SIZE 1024
unsigned char isr_stack[ISR_STACK_SIZE];
unsigned int saved_ss, saved_sp;
void interrupt new_timer_handler() {
asm {
mov saved_ss, ss
mov saved_sp, sp
mov ax, seg isr_stack
mov ss, ax
mov sp, offset isr_stack + ISR_STACK_SIZE
}
// === 中断处理逻辑,现在用的是独立栈 ===
do_interpolation_step();
asm {
mov ss, saved_ss
mov sp, saved_sp
}
outportb(0x20, 0x20);
}
|
第六部分:完整的运动控制 TSR 架构示例
结合上述所有要点,一个线切割机插补控制的 TSR 骨架大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
#include <dos.h>
#include <stdio.h>
/* ---- 全局状态(中断与主循环共享)---- */
volatile long current_x = 0, current_y = 0;
volatile long target_x = 0, target_y = 0;
volatile int running = 1;
void interrupt (*old_timer)();
/* ---- 直线插补的一步(简化的Bresenham思路)---- */
void interrupt timer_isr() {
static long err = 0;
long dx = target_x - current_x;
long dy = target_y - current_y;
if (dx != 0 || dy != 0) {
// 简化的逐点比较插补算法
if (labs(dx) > labs(dy)) {
current_x += (dx > 0) ? 1 : -1;
outportb(0x300, 0x01); // X轴脉冲(示例端口)
} else {
current_y += (dy > 0) ? 1 : -1;
outportb(0x300, 0x02); // Y轴脉冲
}
}
outportb(0x20, 0x20); // EOI
}
void install() {
old_timer = getvect(0x08);
disable();
set_timer_frequency(5000); // 5kHz插补节拍
setvect(0x08, timer_isr);
enable();
}
void restore() {
disable();
set_timer_frequency(18); // 恢复标准18.2Hz(约)
setvect(0x08, old_timer);
enable();
}
int main() {
printf("线切割机插补控制器 - TSR 模式启动\n");
install();
/* 主循环:负责非实时逻辑(G代码解析、显示、通信)*/
while (running) {
parse_next_gcode_line(); // 解析下一条加工指令,更新 target_x/y
update_status_display();
if (kbhit() && getch() == 27) running = 0; // ESC 退出
}
restore();
printf("已恢复系统定时器,安全退出\n");
return 0;
}
|
注意:这个示例作为常驻服务跑(不调用 INT 21h AH=31h 终止),是因为整体程序本身还需要跑主循环处理 G 代码解析。真正的 TSR(终止后仍驻留)更适合"装好就不用管,靠中断在后台默默干活"的场景,比如热键工具、打印后台服务。对于线切割机控制器来说,你可能更需要的是主循环 + 高频中断这种混合架构,而不是严格意义上的传统 TSR。
第七部分:从 TC3/DOS 迁移到 DJGPP 的实时性考量
如果后续要把这类控制程序迁移到 DJGPP(32位保护模式),有几个关键差异需要注意:
| 方面 |
16位实模式(TC3) |
32位保护模式(DJGPP) |
| 中断向量表 |
直接在 0000:0000,setvect/getvect |
需要用 _go32_dpmi_lock_code 锁定中断代码所在页面,避免虚拟内存换出 |
| 中断处理函数 |
void interrupt 关键字 |
需要用 _go32_dpmi_allocate_iret_wrapper 包装真正的 ISR |
| I/O 端口 |
inportb/outportb |
inportb/outportb(DJGPP 也提供,几乎兼容) |
| 内存锁定 |
不需要(DOS本身没有分页) |
必须锁定中断处理代码和栈,否则触发缺页异常时会死锁 |
| 栈切换 |
手工 asm 切换段寄存器 |
更复杂,保护模式段管理不同 |
DJGPP 下写实时中断处理程序难度显著上升,尤其是内存锁定这一步,是很多人在保护模式下写运动控制程序踩坑最多的地方——如果中断处理代码所在的内存页被虚拟内存系统换出到磁盘,下次中断触发时试图执行这段代码会引发页故障,而页故障处理本身又需要中断环境,容易死锁。
考虑到你的场景对实时性要求高、还要跑复杂插补计算,这里有个权衡:继续用 16 位实模式 TC3/汇编写核心的中断服务程序(时序关键的部分),把非实时的部分(G代码解析、UI、通信)迁移到现代工具链,可能比整体迁移到 DJGPP 更稳妥、风险更低。
总结
TSR 技术在运动控制场景下的核心价值
- 没有操作系统调度器抢占 —— 这是选择 DOS 而不是 Linux 的核心原因,中断响应时序完全在你的掌控之中
- 中断向量劫持 —— 让高频定时器中断成为插补计算和脉冲输出的可靠时钟源
- 8253/8254 PIT 重编程 —— 从 18.2Hz 提升到 kHz 级别,满足伺服控制需求
- 中断/主循环分层架构 —— 时间关键的部分放中断里(越简单越好),复杂逻辑放主循环
- 可重入性和临界区保护 —— DOS 本身不可重入,必须小心设计共享数据的访问
这套架构本质上跟现代嵌入式 RTOS 的"中断服务程序 + 任务队列"思想是一致的——只是在 DOS 这个"裸机操作系统"上,你需要自己搭建这套基础设施,而不是依赖内核提供。这也正是 DOS 相比 Linux 在实时控制场景下的优势:没有中间层,没有不确定性,一切尽在掌控。
参考资料
- Ralf Brown’s Interrupt List(INT 08h/INT 21h/INT 33h 完整文档)
- IBM PC 8253/8254 Programmable Interval Timer 数据手册
- Michael Abrash’s Graphics Programming Black Book(大量涉及中断编程实践)
- DOS编程:TSR与设备驱动开发(相关中文技术资料)
相关阅读