1. 项目概述
去年冬天在电子垃圾堆里翻出几个旧光驱,看着里面的步进电机突然有了灵感——何不DIY个写字机器人?说干就干,经过两个月的折腾,终于完成了这个基于STM32F103C8T6的自动绘图装置。整个项目从机械结构设计到电路板绘制,从运动算法实现到上位机通信,踩坑无数但也收获颇丰。
这个写字机器人最核心的特点是"低成本高精度":主控选用10块钱的STM32F103C8T6,机械部分大量使用回收件,最终整机成本控制在200元以内,却能实现0.3mm的绘图精度。特别适合电子爱好者练手,或者给小朋友做个自动抄作业神器(当然不鼓励这么用)。
2. 硬件设计解析
2.1 机械结构设计
核心采用十字滑台结构,X轴使用Φ8mm光轴+直线轴承的方案,Y轴则选择了M8丝杆传动。这种混合设计既保证了X轴的平滑移动,又确保了Y轴的传动精度。实测在300mm行程范围内,重复定位误差不超过±0.2mm。
几个关键细节:
- 导轨安装孔位特意留了0.5mm装配余量,这个经验是用三套报废亚克力板换来的
- 光轴两端用3D打印的支座固定,内部预埋M3螺纹嵌件方便调节平行度
- 笔夹部分采用弹簧复位设计,通过橡皮筋提供约200g的下压力
特别提醒:亚克力板切割后一定要用酒精擦拭切口,否则组装时容易开裂。我在第一版时就因为没做这个处理,装到一半整个框架就裂开了。
2.2 电路设计要点
主控电路围绕STM32F103C8T6搭建,核心外设包括:
- TIM1产生两路PWM分别控制两个A4988驱动模块
- USART1用于接收上位机发送的G代码
- 74HC595扩展IO控制LED状态指示
电源部分特别要注意电机驱动的干扰问题:
c复制// 电源滤波设计
+12V ──┤470uF电解电容├──┤0.1uF瓷片电容├── A4988_VMOT
│(防电压跌落) │ │(高频滤波) │
GND ───┴──────┬───────┴───────┬────┘
│ │
100uH电感 10Ω电阻
│ │
┌──┴──┐ ┌─┴─┐
│电机1│ │电机2│
└─────┘ └───┘
3. 核心算法实现
3.1 运动控制算法
采用改进型Bresenham直线插补算法,关键优化点包括:
- 用定点数运算替代浮点,提升72MHz主频下的计算效率
- 引入速度梯形规划,避免电机启停时的失步现象
- 动态调整脉冲间隔实现恒线速控制
c复制// 优化后的插补算法核心代码
void LineWithAccel(int x1, int y1, int x2, int y2, uint16_t speed) {
int32_t dx = abs(x2 - x1);
int32_t dy = abs(y2 - y1);
int8_t sx = x1 < x2 ? 1 : -1;
int8_t sy = y1 < y2 ? 1 : -1;
int32_t err = dx - dy;
// 速度规划参数
uint16_t step_total = dx + dy;
uint16_t accel_steps = step_total / 4;
uint16_t decel_start = step_total - accel_steps;
for(uint16_t step=0; step<step_total; step++) {
// 速度曲线计算
uint16_t pulse_delay;
if(step < accel_steps) {
pulse_delay = speed + (speed/2)*(accel_steps-step)/accel_steps;
} else if(step > decel_start) {
pulse_delay = speed + (speed/2)*(step-decel_start)/accel_steps;
} else {
pulse_delay = speed;
}
StepperMoveTo(x1, y1);
// 误差计算与坐标更新
int32_t e2 = 2 * err;
if(e2 > -dy) { err -= dy; x1 += sx; }
if(e2 < dx) { err += dx; y1 += sy; }
delay_us(pulse_delay);
}
}
3.2 G代码解析器
通过环形缓冲区实现G代码的实时解析,支持常用指令:
- G00/G01 快速移动/直线插补
- G02/G03 圆弧插补
- M03/M05 抬笔/落笔控制
c复制#define GCODE_BUFFER_SIZE 512
typedef struct {
char buffer[GCODE_BUFFER_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
} GcodeBuffer;
void USART1_IRQHandler(void) {
if(USART1->SR & USART_SR_RXNE) {
gcodeBuf.buffer[gcodeBuf.head++] = USART1->DR;
if(gcodeBuf.head >= GCODE_BUFFER_SIZE) gcodeBuf.head = 0;
}
}
uint8_t GcodeParser(GcodeBuffer *buf) {
if(buf->head == buf->tail) return 0;
static char line[128];
static uint8_t pos = 0;
while(buf->head != buf->tail) {
char c = buf->buffer[buf->tail++];
if(buf->tail >= GCODE_BUFFER_SIZE) buf->tail = 0;
if(c == '\n') {
line[pos] = '\0';
pos = 0;
return ProcessGcodeLine(line);
} else {
line[pos++] = c;
if(pos >= sizeof(line)-1) pos = 0; // 防止溢出
}
}
return 0;
}
4. 关键调试经验
4.1 电机失步问题排查
初期测试时频繁出现电机丢步现象,通过以下步骤最终定位问题:
- 用示波器检查PWM信号波形,发现脉冲间隔不稳定
- 排查发现TIM1时钟源配置错误,实际时钟只有36MHz
- 修正时钟树配置,开启PLL倍频至72MHz
- 在电机电源端增加更大容量电容(最终采用470uF+100nF并联方案)
血泪教训:STM32的定时器时钟源一定要仔细检查,我当初就是没注意APB1和APB2的区别,导致TIM1跑在了一半频率上。
4.2 机械共振抑制
当绘图速度超过50mm/s时,整个机械结构会出现明显振动。通过以下改进措施解决:
- 在亚克力板连接处加贴3M VHB双面胶增加阻尼
- 将步进电机驱动模式从全步进改为1/8微步
- 在运动控制算法中加入S型速度曲线
- 给光轴支座增加橡胶减震垫
实测改进后最大工作速度可提升至80mm/s,且画圆时的锯齿现象明显改善。
5. 实用扩展功能
5.1 Inkscape插件开发
为了方便将矢量图形转换为G代码,我基于Inkscape的扩展机制开发了个简单插件:
python复制#!/usr/bin/env python
import inkex
from lxml import etree
class GcodeExport(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
self.arg_parser.add_argument("--speed", type=int, default=1000)
def effect(self):
gcode = ["G90", "G21", "M05"] # 初始化指令
for node in self.svg.selected.values():
if node.tag == inkex.addNS('path', 'svg'):
path = node.path.to_arrays()
for cmd in path:
if cmd[0] == 'M': # 移动指令
gcode.append(f"G00 X{cmd[1][0]:.2f} Y{cmd[1][1]:.2f}")
elif cmd[0] == 'L': # 直线指令
gcode.append(f"G01 X{cmd[1][0]:.2f} Y{cmd[1][1]:.2f} F{self.options.speed}")
with open(self.options.output, 'w') as f:
f.write('\n'.join(gcode))
if __name__ == '__main__':
GcodeExport().run()
5.2 压力控制模块
后期增加的Z轴压力控制功能,主要实现方案:
- 使用微型直线电位器检测笔尖压力
- 通过STM32的ADC采集压力信号
- PID算法动态调整步进电机电流
- 压力校准程序记录不同材质的最佳参数
c复制#define PRESSURE_AVG 10
uint16_t pressure_samples[PRESSURE_AVG];
uint8_t sample_index = 0;
void ADC1_2_IRQHandler(void) {
pressure_samples[sample_index++] = ADC1->DR;
if(sample_index >= PRESSURE_AVG) sample_index = 0;
uint32_t sum = 0;
for(uint8_t i=0; i<PRESSURE_AVG; i++) {
sum += pressure_samples[i];
}
current_pressure = sum / PRESSURE_AVG;
// PID计算
pressure_error = target_pressure - current_pressure;
pressure_integral += pressure_error;
if(pressure_integral > 1000) pressure_integral = 1000;
else if(pressure_integral < -1000) pressure_integral = -1000;
float output = Kp * pressure_error +
Ki * pressure_integral +
Kd * (pressure_error - last_error);
last_error = pressure_error;
// 调整电机电流
uint16_t current = BASE_CURRENT + (uint16_t)output;
if(current > MAX_CURRENT) current = MAX_CURRENT;
SetMotorCurrent(current);
}
整个项目最让我自豪的是那个用旧光驱电机改造的送纸机构——通过橡胶轮摩擦传动,配合光电传感器实现自动进纸。这个设计不仅零成本,而且可靠性出乎意料的好,连续工作两小时都不会出现卡纸。下次准备尝试加入图像识别功能,让机器人能临摹实物图案,不过那得等我把OV7670摄像头驱动调通再说了。
