1、前述
在学校的时候,有一次参加电赛选的电机夹笔的题目,用的步进电机和导轨,那会吧,步进电机用的少整的不太行,最后拿了个省三,磕碜人。其实现在想想,那个题目也不需要什么算法,就是细节没处理好,这要搁我现在比,高低得拿个。。。。。。
比完赛实验室多出了很多步进电机,看着电机新的一P,又想到这垃圾的比赛成绩,想着用这些电机整点啥,弥补一下缺憾,和同学商量说做个写字机器人吧,说不定还能用的着。之后就研究步进电机的控制算法,因为比完赛没几天就回家了,在家真是没什么效率,三天打渔两天晒网,没干劲,结果这个东西就一直拖着没什么进展。后来逛某站,看到了别人做的写字机器人,无意间了解到GRBL,搜寻了一下发现在其基础上修改就可以实现写字机器人,并且GRBL很成熟呀,都已经更新了好多版本了。到这我就想别自己写算法了,自己的知识储备太少了,实际做起来和无头苍蝇一样四处乱撞,没效率,就先学着用GRBL这个东西吧,如果用都用不明白,何谈自己做呢。然后就打算移植GRBL做写字机器人。
后来都已经开学了,在学校里做好的,3D打印了零部件,虽说不太行,但基本功能也算是实现了。在这里记录移植GRBL到ESP32需要修改哪些部分,说不定能还能帮到别人。
文章目录
- 1、前述
- 2、GRBL中需要修改的部分
- 3、具体修改过程
- 3.1、获取GRBL源代码
- 3.2 添加一个设定引脚的文件
- 3.3、serial.cpp文件相关程序修改
- 3.3、stepper.cpp文件相关程序修改
- 3.4、eeprom.cpp文件相关程序修改
- 3.5、spindle_control.cpp文件相关程序修改
- 3.6、nuts_bolts.cpp文件先关程序修改
- 3.7、probe.cpp文件相关程序修改
- 3.8、coolant_control.cpp文件相关程序修改
- 3.9、limits.cpp文件相关程序修改
- 3.10、system.cpp文件相关程序修改
- 3.11、cpu_map_esp32.h文件修改
- 3.12、nuts_bolts.h文件修改
- 3.14、grbl.h文件修改
- 3.15、config.h文件修改
- 3.16、main.cpp文件修改
- 3.17、调转X、Y、Z轴移动方向
- 4、硬件结构
- 5、上机试验
- 6、总结语
2、GRBL中需要修改的部分
最开始研究的是0.8c版本的,因为使用的是Corexy结构,所以又移植了0.9J版本,这里就以0.9J为主进行介绍。主要需要修改下面列出的这几部分。
(1)串口功能相关代码,实现数据接收、发送功能,在serial.c文件中
(2)步进电机控制功能相关代码,其中包括定时器的初始化、中断函数等,用于发送脉冲控制步进电机转动,在stepper.c文件中。
(3)参数保存功能相关代码,实现参数的掉电保存,在eeprom.c文件中。
(4)主轴控制功能相关代码,控制主轴的状态,在spindle_control.c文件中。
(5)引脚定义,设置相关功能引脚。
(6)脉冲步数、速度等参数设置。
主要就是上面这几部分了,当然也不止上面这几部分,还要很多用不到的功能也需要修改,这里没有提,后面会详细叙述的。
3、具体修改过程
3.1、获取GRBL源代码
代码在Github上开源了,直接搜GRBL就可以找到,其中主分支就是0.9J版本的,直接打包下载即可,链接:GRBL源代码下载地址 。
其中grbl这个文件夹里面就是源代码了,下面是一部分文件的截图,可以看到是.C而不是.Cpp文件。用的platformio,选的arduino框架开发的,为了兼容,我直接把C改成了Cpp文件,不影响使用的。把源代码添加到建立好的工程项目中,接下来就是对涉及到寄存器的代码进行修改适配。
3.2 添加一个设定引脚的文件
为了方便修改相关引脚,建立一个H文件存放引脚的定义。下载的源文件中有个cpu_map文件夹,里面有两个H文件,对应的是不同芯片和不同的引脚。在cpu_map文件夹下新建一个H文件,为了方便使用命名为cpu_map_esp32.h,让我们知道是和ESP32有关的。
打开config.h文件将41行的宏定义修改成如下代码。
#define CPU_MAP_ESP32
然后打开cpu_map.h文件添加如下代码。
#ifdef CPU_MAP_ESP32
#include "cpu_map/cpu_map_esp32.h"
#endif
通过上面添加的代码能猜到,后续只要修改config.h文件文件中的关于CPU的宏定义就能适配不同的CPU。因为宏定义不对,所调用的包括了引脚定义的H文件也不同。
3.3、serial.cpp文件相关程序修改
这个文件中存放的是串口相关的函数,其中包括串口接收、发送函数,我们知道写字机器使用串口发送数据的(当然也有使用wifi或其他方式),实际程序执行过程就是通过串口接收数据保存到缓存区,对缓存区的数据进行提取获得符合格式的数据(不符合的数据在这一环节中会被删除),根据数据执行对应的命令,比如XYZ轴的移动,主控的控制,参数设置等等。执行完对应的动作后也会通过串口发送"OK"或其他字符,所以说串口就是GRBL控制器和外界进行数据交换的桥梁,非常重要。
源代码中使用的是中断接收、发送数据的,但我发现这Arduino好像没有用串口中断接收、发送数据的函数呀,没找到,最后翻没有经过封装的底层API才找到使用中断接收数据的方法,但其实也没必要使用中断,下面就不提了,怪麻烦的。
3.3.1、serial_init()函数修改
这是串口的初始化函数,将其中的代码修改成ESP32的串口初始化代码即可。下面是源代码中的程序。
void serial_init()
{
// Set baud rate
#if BAUD_RATE < 57600
uint16_t UBRR0_value = ((F_CPU / (8L * BAUD_RATE)) - 1)/2 ;
UCSR0A &= ~(1 << U2X0); // baud doubler off - Only needed on Uno XXX
#else
uint16_t UBRR0_value = ((F_CPU / (4L * BAUD_RATE)) - 1)/2;
UCSR0A |= (1 << U2X0); // baud doubler on for high baud rates, i.e. 115200
#endif
UBRR0H = UBRR0_value >> 8;
UBRR0L = UBRR0_value;
// enable rx and tx
UCSR0B |= 1<<RXEN0;
UCSR0B |= 1<<TXEN0;
// enable interrupt on complete reception of a byte
UCSR0B |= 1<<RXCIE0;
// defaults to 8-bit, no parity, 1 stop bit
}
下面是修改后的程序。
void serial_init()
{
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
// Set baud rate
#if BAUD_RATE < 57600
uint16_t UBRR0_value = ((F_CPU / (8L * BAUD_RATE)) - 1)/2 ;
UCSR0A &= ~(1 << U2X0); // baud doubler off - Only needed on Uno XXX
#else
uint16_t UBRR0_value = ((F_CPU / (4L * BAUD_RATE)) - 1)/2;
UCSR0A |= (1 << U2X0); // baud doubler on for high baud rates, i.e. 115200
#endif
UBRR0H = UBRR0_value >> 8;
UBRR0L = UBRR0_value;
// enable rx and tx
UCSR0B |= 1<<RXEN0;
UCSR0B |= 1<<TXEN0;
// enable interrupt on complete reception of a byte
UCSR0B |= 1<<RXCIE0;
// defaults to 8-bit, no parity, 1 stop bit
#endif
#ifdef CPU_MAP_ESP32
Serial.begin(115200); //串口初始化
#endif
}
修改后的代码使用条件编译命令进行修改,通过修改宏定义就可以更改需要编译的代码段,非常的方便。这样也方便查看初始代码,方便对照进行修改。在3.2节中定义了CPU_MAP_ESP32,而CPU_MAP_ATMEGA328P和CPU_MAP_ATMEGA2560没有被定义,所以会编译对应的ESP32相关代码。要是觉得上面的代码用不到且碍眼,完全可以删掉,这里是觉得这样修改可能会好一点。在这个页面看不出哪段代码是符合条件的,下面放一张使用使用vscode打开的页面图,有代码高亮等非常实用的功能,看起来就非常明了。不像那吊毛keil5,都看瞎眼了。
从上图可以清晰的看出60-79行程序更暗,其不满足判断条件不会被编译,而82-84行是会被编译的。所以说呀骚年,vscode+platformio赶紧用起来,一个好的开发工具真是事半功倍呀,效率大大的提高。
3.3.2、修改serial_write()函数程序
void serial_write(uint8_t data)
{
#ifdef CPU_MAP_ESP32
Serial.write(data); //发送一个字节
#endif
}
下面的程序就直接把用不到的删了,不然太占地方了,不便于阅读。
修改后只需要一行程序,因为这个函数就是使用串口发送一个字节,而原先程序使用的是环形缓冲区发送字节数据的,而且用的中断,因为本次修改用不到中断,直接发就行了,所以改完程序更少了。
3.3.3、屏蔽ISR(SERIAL_UDRE)中断回调函数
因为没有使用到串口中断,所有这个函数可以直接删掉,或者像3.3.1节中使用条件编译语句被屏蔽掉。
3.3.4、修改ISR(SERIAL_RX)串口中断接收函数
原先的ISR(SERIAL_RX)函数删掉或者屏蔽掉。添加一个新的函数使用串口查询的方式实现数据接收并保存到缓存区,函数如下。这个函数名字无所谓,记得在H文件中声明此函数。
#ifdef CPU_MAP_ESP32
void rx_buffer(void)
{
uint8_t data;
uint8_t next_head;
while(Serial.available())
{
data = Serial.read();
// Pick off realtime command characters directly from the serial stream. These characters are
// not passed into the buffer, but these set system state flag bits for realtime execution.
switch (data) {
case CMD_STATUS_REPORT: bit_true_atomic(sys_rt_exec_state, EXEC_STATUS_REPORT); break; // Set as true
case CMD_CYCLE_START: bit_true_atomic(sys_rt_exec_state, EXEC_CYCLE_START); break; // Set as true
case CMD_FEED_HOLD: bit_true_atomic(sys_rt_exec_state, EXEC_FEED_HOLD); break; // Set as true
case CMD_SAFETY_DOOR: bit_true_atomic(sys_rt_exec_state, EXEC_SAFETY_DOOR); break; // Set as true
case CMD_RESET: mc_reset(); break; // Call motion control reset routine.
default: // Write character to buffer
next_head = serial_rx_buffer_head + 1;
if (next_head == RX_BUFFER_SIZE) { next_head = 0; }
// Write data to buffer unless it is full.
if (next_head != serial_rx_buffer_tail) {
serial_rx_buffer[serial_rx_buffer_head] = data;
serial_rx_buffer_head = next_head;
}
#ifdef ENABLE_XONXOFF
if ((serial_get_rx_buffer_count() >= RX_BUFFER_FULL) && flow_ctrl == XON_SENT) {
flow_ctrl = SEND_XOFF;
UCSR0B |= (1 << UDRIE0); // Force TX
}
#endif
//TODO: else alarm on overflow?
}
}
}
#endif
这个函数功能是检测串口是否接收到数据,有的话对数据进行检测,如果是功能符号则会执行对应的功能(在config.h文件的49-59行被定义),如果不是则会保存数据缓存区。
3.3.5、修改serial_read()串口数据读取函数
需要将3.3.4节新建的rx_buffer()函数添加到serial_read()函数中,函数如下。
uint8_t serial_read()
{
rx_buffer(); //检测串口是否接收到数据
uint8_t tail = serial_rx_buffer_tail; // Temporary serial_rx_buffer_tail (to optimize for volatile)
if (serial_rx_buffer_head == tail) {
return SERIAL_NO_DATA;
} else {
uint8_t data = serial_rx_buffer[tail];
tail++;
if (tail == RX_BUFFER_SIZE) { tail = 0; }
serial_rx_buffer_tail = tail;
#ifdef ENABLE_XONXOFF
if ((serial_get_rx_buffer_count() < RX_BUFFER_LOW) && flow_ctrl == XOFF_SENT) {
flow_ctrl = SEND_XON;
UCSR0B |= (1 << UDRIE0); // Force TX
}
#endif
return data;
}
}
为什么串口接收函数要加到这个地方呢。看过源程序就会知道,程序中会一直调用serial_read()检测数据缓存区是否还有数据。因为原先是串口中断接收,现在使用的查询的方式接收,那么就需要一直执行这个串口接收数据的函数,也就是3.3.4节定义的rx_buffer()函数,所以把将其放到了会经常被调用的serial_read()函数中。
到此serial文件中的相关程序就修改好了。
3.3、stepper.cpp文件相关程序修改
3.3.1、修改stepper_init()定时器传输函数
定时器和IO口的初始化就是在这个函数里完成的,需要两个定时器,一个控制IO口输出脉冲,另一个控制输出脉冲的频率,两个定时器协调工作完成对步进电机的控制。应该是这样的吧。程序如下。
hw_timer_t *timer1 = NULL;
hw_timer_t *timer2 = NULL;//记得声明一下
#ifdef CPU_MAP_ESP32
void stepper_init()
{
pinMode(X_STEP_PORT,OUTPUT);
pinMode(Y_STEP_PORT,OUTPUT);
pinMode(Z_STEP_PORT,OUTPUT);
pinMode(X_DIRECTION_PORT,OUTPUT);
pinMode(Y_DIRECTION_PORT,OUTPUT);
pinMode(Z_DIRECTION_PORT,OUTPUT);
pinMode(STEPPERS_DISABLE_BIT,OUTPUT);
digitalWrite(STEPPERS_DISABLE_BIT,HIGH);
timer1 = timerBegin(0, 8, true); // 80M的时钟 8分频 10M
timerStop(timer1);
timerAttachInterrupt(timer1, &TIMER1_COMPA_vect, true);
timerAlarmWrite(timer1, 10000, true);
timerAlarmEnable(timer1);
timer2 = timerBegin(1, 8, true); // 80M的时钟 8分频 10M
timerStop(timer2);
timerAttachInterrupt(timer2, &TIMER2_OVF_vect, true);
timerAlarmWrite(timer2, 10000, true);
timerAlarmEnable(timer2);
}
#endif
初始化脉冲输出引脚、方向控制引脚和驱动器使能引脚,初始化两个定时器。到这时候会发现X_STEP_PORT、Y_STEP_PORT等符号没有定义,这个后面会定义的,这里先不管。这什么能整完呀,真麻烦。
3.3.2、修改定时器1中断函数
删除或者屏蔽掉原先ISR(TIMER1_COMPA_vect)函数,这里不在函数内部改了,直接新建一个函数TIMER1_COMPA_vect(),和3.3.1节中定时器1注册的中断函数是对应的。记得声明函数,程序如下。
#ifdef CPU_MAP_ESP32
void IRAM_ATTR TIMER1_COMPA_vect()
{
// SPINDLE_ENABLE_PORT ^= 1<<SPINDLE_ENABLE_BIT; // Debug: Used to time ISR
if (busy) { return;}// The busy-flag is used to avoid reentering this interrupt
// Set the direction pins a couple of nanoseconds before we step the steppers
digitalWrite(X_DIRECTION_PORT,st.dir_outbits & bit(X_DIRECTION_BIT));
digitalWrite(Y_DIRECTION_PORT,st.dir_outbits & bit(Y_DIRECTION_BIT));
digitalWrite(Z_DIRECTION_PORT,st.dir_outbits & bit(Z_DIRECTION_BIT));
// Then pulse the stepping pins
#ifdef STEP_PULSE_DELAY
// st.step_bits = (STEP_PORT & ~STEP_MASK) | st.step_outbits; // Store out_bits to prevent overwriting.
#else // Normal operation
digitalWrite(X_STEP_PORT,st.step_outbits & bit(X_STEP_BIT));
digitalWrite(Y_STEP_PORT,st.step_outbits & bit(Y_STEP_BIT));
digitalWrite(Z_STEP_PORT,st.step_outbits & bit(Z_STEP_BIT));
#endif
// Enable step pulse reset timer so that The Stepper Port Reset Interrupt can reset the signal after
// exactly settings.pulse_microseconds microseconds, independent of the main Timer1 prescaler.
timerAlarmWrite(timer2,st.step_pulse_time,true);
timerStart(timer2);
busy = true;
sei(); // Re-enable interrupts to allow Stepper Port Reset Interrupt to fire on-time.
// NOTE: The remaining code in this ISR will finish before returning to main program.
// If there is no step segment, attempt to pop one from the stepper buffer
if (st.exec_segment == NULL) {
// Anything in the buffer? If so, load and initialize next step segment.
if (segment_buffer_head != segment_buffer_tail) {
// Initialize new step segment and load number of steps to execute
st.exec_segment = &segment_buffer[segment_buffer_tail];
#ifndef ADAPTIVE_MULTI_AXIS_STEP_SMOOTHING
// With AMASS is disabled, set timer prescaler for segments with slow step frequencies (< 250Hz).
TCCR1B = (TCCR1B & ~(0x07<<CS10)) | (st.exec_segment->prescaler<<CS10);
#endif
// Initialize step segment timing per step and load number of steps to execute.
timerAlarmWrite(timer1,st.exec_segment->cycles_per_tick>>1,true);
st.step_count = st.exec_segment->n_step; // NOTE: Can sometimes be zero when moving slow.
// If the new segment starts a new planner block, initialize stepper variables and counters.
// NOTE: When the segment data index changes, this indicates a new planner block.
if ( st.exec_block_index != st.exec_segment->st_block_index ) {
st.exec_block_index = st.exec_segment->st_block_index;
st.exec_block = &st_block_buffer[st.exec_block_index];
// Initialize Bresenham line and distance counters
st.counter_x = st.counter_y = st.counter_z = (st.exec_block->step_event_count >> 1);
}
st.dir_outbits = st.exec_block->direction_bits ^ dir_port_invert_mask;
#ifdef ADAPTIVE_MULTI_AXIS_STEP_SMOOTHING
// With AMASS enabled, adjust Bresenham axis increment counters according to AMASS level.
st.steps[X_AXIS] = st.exec_block->steps[X_AXIS] >> st.exec_segment->amass_level;
st.steps[Y_AXIS] = st.exec_block->steps[Y_AXIS] >> st.exec_segment->amass_level;
st.steps[Z_AXIS] = st.exec_block->steps[Z_AXIS] >> st.exec_segment->amass_level;
#endif
} else {
// Segment buffer empty. Shutdown.
st_go_idle();
bit_true_atomic(sys_rt_exec_state,EXEC_CYCLE_STOP); // Flag main program for cycle end
return; // Nothing to do but exit.
}
}
// Check probing state.
probe_state_monitor();
// Reset step out bits.
st.step_outbits = 0;
// Execute step displacement profile by Bresenham line algorithm
#ifdef ADAPTIVE_MULTI_AXIS_STEP_SMOOTHING
st.counter_x += st.steps[X_AXIS];
#else
st.counter_x += st.exec_block->steps[X_AXIS];
#endif
if (st.counter_x > st.exec_block->step_event_count) {
st.step_outbits |= (1<<X_STEP_BIT);
st.counter_x -= st.exec_block->step_event_count;
if (st.exec_block->direction_bits & (1<<X_DIRECTION_BIT)) { sys.position[X_AXIS]--; }
else { sys.position[X_AXIS]++; }
}
#ifdef ADAPTIVE_MULTI_AXIS_STEP_SMOOTHING
st.counter_y += st.steps[Y_AXIS];
#else
st.counter_y += st.exec_block->steps[Y_AXIS];
#endif
if (st.counter_y > st.exec_block->step_event_count) {
st.step_outbits |= (1<<Y_STEP_BIT);
st.counter_y -= st.exec_block->step_event_count;
if (st.exec_block->direction_bits & (1<<Y_DIRECTION_BIT)) { sys.position[Y_AXIS]--; }
else { sys.position[Y_AXIS]++; }
}
#ifdef ADAPTIVE_MULTI_AXIS_STEP_SMOOTHING
st.counter_z += st.steps[Z_AXIS];
#else
st.counter_z += st.exec_block->steps[Z_AXIS];
#endif
if (st.counter_z > st.exec_block->step_event_count) {
st.step_outbits |= (1<<Z_STEP_BIT);
st.counter_z -= st.exec_block->step_event_count;
if (st.exec_block->direction_bits & (1<<Z_DIRECTION_BIT)) { sys.position[Z_AXIS]--; }
else { sys.position[Z_AXIS]++; }
}
// During a homing cycle, lock out and prevent desired axes from moving.
if (sys.state == STATE_HOMING) { st.step_outbits &= sys.homing_axis_lock; }
st.step_count--; // Decrement step events count
if (st.step_count == 0) {
// Segment is complete. Discard current segment and advance segment indexing.
st.exec_segment = NULL;
if ( ++segment_buffer_tail == SEGMENT_BUFFER_SIZE) { segment_buffer_tail = 0; }
}
st.step_outbits ^= step_port_invert_mask; // Apply step port invert mask
busy = false;
// SPINDLE_ENABLE_PORT ^= 1<<SPINDLE_ENABLE_BIT; // Debug: Used to time ISR
}
#endif
3.3.3、修改定时器2中断函数
删除或屏蔽掉原先的ISR(TIMER0_OVF_vect)函数,直接新建一个函数TIMER2_OVF_vect(),和3.3.1节中定时器2注册的中断函数是对应的。记得声明函数,程序如下。
#ifdef CPU_MAP_ESP32
void IRAM_ATTR TIMER2_OVF_vect()
{
// Reset stepping pins (leave the direction pins)
digitalWrite(X_STEP_PORT,LOW);
digitalWrite(Y_STEP_PORT,LOW);
digitalWrite(Z_STEP_PORT,LOW);
timerStop(timer2);
}
#endif
3.3.4、修改st_wake_up()函数
将函数内容进行修改,主要是修改涉及到的定时器部分代码,程序如下。
#ifdef CPU_MAP_ESP32
void st_wake_up()
{
// Enable stepper drivers.
if (bit_istrue(settings.flags,BITFLAG_INVERT_ST_ENABLE)) {
digitalWrite(STEPPERS_DISABLE_BIT,HIGH);
}
else {
digitalWrite(STEPPERS_DISABLE_BIT,LOW);
}
if (sys.state & (STATE_CYCLE | STATE_HOMING)){
// Initialize stepper output bits
st.dir_outbits = dir_port_invert_mask;
st.step_outbits = step_port_invert_mask;
// Initialize step pulse timing from settings. Here to ensure updating after re-writing.
#ifdef STEP_PULSE_DELAY
// // Set total step pulse time after direction pin set. Ad hoc computation from oscilloscope.
// st.step_pulse_time = -(((settings.pulse_microseconds+STEP_PULSE_DELAY-2)*TICKS_PER_MICROSECOND) >> 3);
// // Set delay between direction pin write and step command.
// OCR0A = -(((settings.pulse_microseconds)*TICKS_PER_MICROSECOND) >> 3);
#else // Normal operation
// Set step pulse time. Ad hoc computation from oscilloscope. Uses two's complement.
st.step_pulse_time = settings.pulse_microseconds-2;
#endif
// Enable Stepper Driver Interrupt
timerStart(timer1);
}
}
#endif
3.3.5、修改st_go_idle()函数
将函数内容进行修改,主要是修改涉及到的定时器部分代码,程序如下。
#ifdef CPU_MAP_ESP32
void st_go_idle()
{
// Disable Stepper Driver Interrupt. Allow Stepper Port Reset Interrupt to finish, if active.
timerStop(timer1);
busy = false;
// Set stepper driver idle state, disabled or enabled, depending on settings and circumstances.
bool pin_state = false; // Keep enabled.
if (((settings.stepper_idle_lock_time != 0xff) || sys_rt_exec_alarm) && sys.state != STATE_HOMING) {
// Force stepper dwell to lock axes for a defined amount of time to ensure the axes come to a complete
// stop and not drift from residual inertial forces at the end of the last movement.
delay_ms(settings.stepper_idle_lock_time);
pin_state = true; // Override. Disable steppers.
}
if (bit_istrue(settings.flags,BITFLAG_INVERT_ST_ENABLE)) { pin_state = !pin_state; } // Apply pin invert.
if (pin_state) {
digitalWrite(STEPPERS_DISABLE_BIT,HIGH);
}
else {
digitalWrite(STEPPERS_DISABLE_BIT,LOW);
}
}
#endif
3.5.6、修改st_reset()函数
void st_reset()
{
// Initialize stepper driver idle state.
st_go_idle();
// Initialize stepper algorithm variables.
memset(&prep, 0, sizeof(st_prep_t));
memset(&st, 0, sizeof(stepper_t));
st.exec_segment = NULL;
pl_block = NULL; // Planner block pointer used by segment buffer
segment_buffer_tail = 0;
segment_buffer_head = 0; // empty = tail
segment_next_head = 1;
busy = false;
st_generate_step_dir_invert_masks();
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
// Initialize step and direction port pins.
STEP_PORT = (STEP_PORT & ~STEP_MASK) | step_port_invert_mask;
DIRECTION_PORT = (DIRECTION_PORT & ~DIRECTION_MASK) | dir_port_invert_mask;
#endif
#ifdef CPU_MAP_ESP32
digitalWrite(X_DIRECTION_PORT,st.dir_outbits & bit(X_DIRECTION_BIT));
digitalWrite(Y_DIRECTION_PORT,st.dir_outbits & bit(Y_DIRECTION_BIT));
digitalWrite(Z_DIRECTION_PORT,st.dir_outbits & bit(Z_DIRECTION_BIT));
digitalWrite(X_STEP_PORT,st.step_outbits & bit(X_STEP_BIT));
digitalWrite(Y_STEP_PORT,st.step_outbits & bit(Y_STEP_BIT));
digitalWrite(Z_STEP_PORT,st.step_outbits & bit(Z_STEP_BIT));
#endif
}
到此stepper文件中的相关程序就修改好了。
3.4、eeprom.cpp文件相关程序修改
3.4.1、添加eeprom_init()初始化函数
首先添加一个eeprom_init()初始化函数,用于初始化ESP32的eeprom功能。程序如下。
#ifdef CPU_MAP_ESP32
void eeprom_init(void)
{
EEPROM.begin(1024); //申请1K的eeprom空间
}
#endif
3.4.2、修改eeprom_get_char()函数
修改后的程序如下。
#ifdef CPU_MAP_ESP32
unsigned char eeprom_get_char(unsigned int addr)
{
return EEPROM.readByte(addr);
}
#endif
3.4.3、修改eeprom_put_char()函数
#ifdef CPU_MAP_ESP32
void eeprom_put_char(uint32_t addr,uint8_t new_value)
{
EEPROM.writeByte(addr,new_value);
EEPROM.commit(); //写入数据后必须加该函数,否则数据不会被保存到eeprom
}
#endif
到此eeprom文件中的相关程序就修改好了。
3.5、spindle_control.cpp文件相关程序修改
3.5.1、修改spindle_init()主轴初始化函数
因为我使用的是舵机,所以在这个初始化函数中初始化的是ESP32 的pwm功能,这个地方吧,不同的情况要做不同的修改。高速写字机Z轴用的都是步进电机,用舵机的成本低点但是不如步进电机好用,但是整体可以做的更小。
#ifdef CPU_MAP_ESP32
void spindle_init()
{
ledcSetup(1,50,8); //50HZ-20ms
ledcAttachPin(SPINDLE_CONTROL_BIT,1); //GPIO 15
spindle_stop();
}
#endif
3.5.2、修改spindle_stop()函数
这个地方也需要根据实际情况来修改,
#ifdef CPU_MAP_ESP32
void spindle_stop()
{
ledcWrite(1,26); // 25 抬笔
}
#endif
3.5.3、修改spindle_set_state()函数
这个地方也需要根据实际情况来修改。
#ifdef CPU_MAP_ESP32
void spindle_set_state(uint8_t state, float rpm)
{
// Halt or set spindle direction and rpm.
if (state == SPINDLE_DISABLE) {
spindle_stop(); //抬笔
} else {
ledcWrite(1,21); //放笔
}
}
#endif
此版本的GRBL好像是支持输出PWM的,但我不是很清楚那样应该怎么改。所以就像上面的函数中根据主轴的状态来控制舵机的转动角度实现抬笔和放笔了。不同的上位机主轴的控制命令还不一样,就以用到的为例,微雕管家中使用的是M03和M05命令,一个是使能主轴,一个是不使能,通过在程序的修改,当使能主轴时舵机放笔,不使能时舵机抬笔。而炽写中舵机使用的是M03S_命令,抬笔和放笔时S数值不同,比如放笔时是M03S0,抬笔时是M03S2。如果使用步进电机的话,就可以直接使用Z轴控制命令了,所以说这个不是固定的,但只要我们知道了原理,那不管上位机发什么,都能实现对应的功能。
3.6、nuts_bolts.cpp文件先关程序修改
3.6.1、修改delay_ms()延时函数
这个延时函数不能使用delay(),因为delay()在中断中不起作用。
#ifdef CPU_MAP_ESP32
void delay_ms(uint16_t ms)
{
delayMicroseconds(ms*1000);
}
#endif
3.6.2、修改delay_us()延时函数
#ifdef CPU_MAP_ESP32
void delay_us(uint32_t us)
{
delayMicroseconds(us);
}
#endif
3.7、probe.cpp文件相关程序修改
这个文件里面是有关“探针”的,应该是用在CNC上的,比较GRBL原来就是用于CNC雕刻机的。因为用不到,所以这一部分就不修改添加代码了,直接将设计到寄存器的程序屏蔽掉。要是自己觉得用的到,自己在修改吧。
3.7.1 修改probe_init()函数
void probe_init()
{
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
PROBE_DDR &= ~(PROBE_MASK); // Configure as input pins
#ifdef DISABLE_PROBE_PIN_PULL_UP
PROBE_PORT &= ~(PROBE_MASK); // Normal low operation. Requires external pull-down.
#else
PROBE_PORT |= PROBE_MASK; // Enable internal pull-up resistors. Normal high operation.
#endif
// probe_configure_invert_mask(false); // Initialize invert mask. Not required. Updated when in-use.
#endif
}
上面这个函数中的程序使用条件编译语句进行屏蔽了,做写字机根本用不到,直接删除也行的。删除的时候删除函数里的内容就行,别直接把函数删了,不然在其他地方调用这个函数的话,就要报错了。
3.7.2、修改probe_get_state()函数
uint8_t probe_get_state() {
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
return((PROBE_PIN & PROBE_MASK) ^ probe_invert_mask);
#endif
return 1;
}
这个函数也是,将涉及到寄存器的程序屏蔽掉,为了防止编译出现警告,写了一个return。
3.7.3、修改probe_configure_invert_mask()函数
void probe_configure_invert_mask(uint8_t is_probe_away)
{
probe_invert_mask = 0; // Initialize as zero.
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
if (bit_isfalse(settings.flags,BITFLAG_INVERT_PROBE_PIN)) { probe_invert_mask ^= PROBE_MASK; }
if (is_probe_away) { probe_invert_mask ^= PROBE_MASK; }
#endif
}
屏蔽了就完了,用不到的就屏蔽掉,啊啊啊啊。
3.8、coolant_control.cpp文件相关程序修改
这个文件里面是有关“冷却剂”的,说实话根本用不到,直接将设计到寄存器的程序屏蔽掉。要是自己觉得用的到,自己在修改吧。
3.8.1、修改coolant_init()函数
void coolant_init()
{
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
COOLANT_FLOOD_DDR |= (1 << COOLANT_FLOOD_BIT);
#ifdef ENABLE_M7
COOLANT_MIST_DDR |= (1 << COOLANT_MIST_BIT);
#endif
#endif
coolant_stop();
}
3.8.2、修改coolant_stop()函数
void coolant_stop()
{
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
COOLANT_FLOOD_PORT &= ~(1 << COOLANT_FLOOD_BIT);
#ifdef ENABLE_M7
COOLANT_MIST_PORT &= ~(1 << COOLANT_MIST_BIT);
#endif
#endif
}
3.8.2、修改coolant_set_state()函数
void coolant_set_state(uint8_t mode)
{
if (mode == COOLANT_FLOOD_ENABLE) {
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
COOLANT_FLOOD_PORT |= (1 << COOLANT_FLOOD_BIT);
#endif
#ifdef ENABLE_M7
} else if (mode == COOLANT_MIST_ENABLE) {
COOLANT_MIST_PORT |= (1 << COOLANT_MIST_BIT);
#endif
} else {
coolant_stop();
}
}
上面这个函数中的程序使用条件编译语句进行屏蔽了,做写字机根本用不到,直接删除也行的。删除的时候删除函数里的内容就行,别直接把函数删了,不然在其他地方调用这个函数的话,就要报错了。
3.9、limits.cpp文件相关程序修改
这个文件里面是有关限位、归位的,话说在前头,这个里面吧我感觉也用不到,那么多代码,在上面修改还不如直接不用这个里面的限位,不如直接自己编一个,限位和归位的程序,我是自己写的归位的,所以这个里面我就直接屏蔽了。要是自己觉得用的到,自己在修改吧。
3.9.1、修改limits_init()函数
void limits_init()
{
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
LIMIT_DDR &= ~(LIMIT_MASK); // Set as input pins
#ifdef DISABLE_LIMIT_PIN_PULL_UP
LIMIT_PORT &= ~(LIMIT_MASK); // Normal low operation. Requires external pull-down.
#else
LIMIT_PORT |= (LIMIT_MASK); // Enable internal pull-up resistors. Normal high operation.
#endif
if (bit_istrue(settings.flags,BITFLAG_HARD_LIMIT_ENABLE)) {
LIMIT_PCMSK |= LIMIT_MASK; // Enable specific pins of the Pin Change Interrupt
PCICR |= (1 << LIMIT_INT); // Enable Pin Change Interrupt
} else {
limits_disable();
}
#ifdef ENABLE_SOFTWARE_DEBOUNCE
MCUSR &= ~(1<<WDRF);
WDTCSR |= (1<<WDCE) | (1<<WDE);
WDTCSR = (1<<WDP0); // Set time-out at ~32msec.
#endif
#endif
}
3.9.2、修改limits_disable()函数
void limits_disable()
{
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
LIMIT_PCMSK &= ~LIMIT_MASK; // Disable specific pins of the Pin Change Interrupt
PCICR &= ~(1 << LIMIT_INT); // Disable Pin Change Interrupt
#endif
}
3.9.3、修改ISR(LIMIT_INT_vect)函数
这个中断函数没用到,直接删除或者屏蔽了就行。这里就不列了,占地方。
3.9.4、修改limits_get_state()函数
uint8_t limits_get_state()
{
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
uint8_t limit_state = 0;
uint8_t pin = (LIMIT_PIN & LIMIT_MASK);
#ifdef INVERT_LIMIT_PIN_MASK
pin ^= INVERT_LIMIT_PIN_MASK;
#endif
if (bit_isfalse(settings.flags,BITFLAG_INVERT_LIMIT_PINS)) { pin ^= LIMIT_MASK; }
if (pin) {
uint8_t idx;
for (idx=0; idx<N_AXIS; idx++) {
if (pin & get_limit_pin_mask(idx)) { limit_state |= (1 << idx); }
}
}
return(limit_state);
#endif
return 0;
}
因为没用到限位功能,根据注释可以知道限位触发时为1未触发为0,所以这个获取限位状态的函数直接返回0,默认为未触发。
3.10、system.cpp文件相关程序修改
3.10.1、修改system_init()函数
这个文件里面也好杂呀,直接删除或屏蔽了。
ESP32的底层代码中好像有一个函数就叫system_init(),如果不修改的话会就冲突报错,所有将本文件中的system_init()函数名修改成grbl_system_init(),将H文件中的也改了,这样就不会报错。修改后程序如下。
void grbl_system_init() //原先函数名为system_init()
{
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
CONTROL_DDR &= ~(CONTROL_MASK); // Configure as input pins
#ifdef DISABLE_CONTROL_PIN_PULL_UP
CONTROL_PORT &= ~(CONTROL_MASK); // Normal low operation. Requires external pull-down.
#else
CONTROL_PORT |= CONTROL_MASK; // Enable internal pull-up resistors. Normal high operation.
#endif
CONTROL_PCMSK |= CONTROL_MASK; // Enable specific pins of the Pin Change Interrupt
PCICR |= (1 << CONTROL_INT); // Enable Pin Change Interrupt
#endif
}
3.10.2、修改ISR(CONTROL_INT_vect) 函数
这个函数吧,屏蔽了就好,这里就不列了。
3.11、cpu_map_esp32.h文件修改
从3.1到3.10,源文件中的程序差不多修改完了,接下来是设定相关引脚,全部在cpu_map_esp32.h文件中。
#ifndef __cpu_map_esp32_h
#define __cpu_map_esp32_h
// Define pin-assignments
// NOTE: All step bit and direction pins must be on the same port.
// 步进电机控制端口,脉冲输出端口和方向控制端口
#define X_STEP_BIT 0
#define Y_STEP_BIT 1
#define Z_STEP_BIT 2
#define X_DIRECTION_BIT 3
#define Y_DIRECTION_BIT 4
#define Z_DIRECTION_BIT 5
#define X_STEP_PORT 26
#define X_DIRECTION_PORT 27
#define Y_STEP_PORT 16
#define Y_DIRECTION_PORT 17
#define Z_STEP_PORT 18
#define Z_DIRECTION_PORT 19
#define STEP_MASK ((1<<X_STEP_BIT)|(1<<Y_STEP_BIT)|(1<<Z_STEP_BIT)) // All step bits
#define DIRECTION_MASK ((1<<X_DIRECTION_BIT)|(1<<Y_DIRECTION_BIT)|(1<<Z_DIRECTION_BIT)) // All direction bits
#define STEPPING_MASK (STEP_MASK | DIRECTION_MASK) // All stepping-related bits (step/direction)
//步进电机驱动器使能控制端口
#define STEPPERS_DISABLE_BIT 21
#define STEPPERS_DISABLE_MASK (1<<STEPPERS_DISABLE_BIT)
// 控制舵机引脚
#define SPINDLE_CONTROL_BIT 4
//限位引脚
#define X_LIMIT_GPIO_PIN_LEFT 23 //引脚
#define X_LIMIT_GPIO_PIN_RIGHT 33 //引脚
#define Y_LIMIT_GPIO_PIN 2 //引脚
#define Z_LIMIT_GPIO_PIN 35 //引脚
//原平台适配,可不用修改。
#define X_LIMIT_BIT 1
#define Y_LIMIT_BIT 2
#define Z_LIMIT_BIT 3
#define LIMIT_MASK ((1<<X_LIMIT_BIT)|(1<<Y_LIMIT_BIT)|(1<<Z_LIMIT_BIT))// All limit bits
#endif
上面就是用到的相关引脚了,没用到的都没定义。和原先的有些不一样,添加了X_STEP_PORT、Y_STEP_PORT、X_DIRECTION_PORT等等定义,为什么要这样改呢。因为使用的引脚不连续,且引脚数有26、27等,不像原先的引脚就是2-7,用一个u8变量就可以储存其对应的所有位,也算是掩码,但是ESP32的引脚不连续,且引脚号过大,所以出次下策,多定义了一个代表端口号的常量,这样的话,XYZ三轴每一个引脚对应着两个两个常量,一个是其在STEP_MASK中的位置,一个是其对应的真是端口号,说的可能很拗口,但是根据stepper那个文件中添加的控制IO口输出脉冲的程序可以看出,在定时器中断程序中根据st.step_outbits这个变量中对应的引脚掩码位来判断该引脚应该输出高电平还是低电平,根据对呀的位,在控制对应的引脚进行输出。不理解的话就算了,反正知道修改对应的引脚应该修改哪就行,应该修改以PORT结尾的常量,不是以BIT结尾的常量。我是这样改的,也不晓得有没有其他的好办法,如果你能保证所用的引脚号在0-7,那么就不需要在增加以PORT结尾的常量。反正就这意思,不说了。
不止有步进电机的引脚,还有驱动器使能引脚,舵机控制引脚,限位引脚等。限位引脚我是用在了自己编写的归位程序中,并不没有适配原先的归位程序,因为我看不懂。
3.12、nuts_bolts.h文件修改
因为Arduino.h文件中有bit(n)的定义,所以需要把nuts_bolts.h文件中定义bit(n)的程序删除或者屏蔽。需要屏蔽的程序如下。
// #define bit(n) (1 << n) //将该行程序屏蔽
还需要对下面这三行程序进行修改,因为涉及到寄存器了。
#define bit_true_atomic(x,mask) {uint8_t sreg = SREG; cli(); (x) |= (mask); SREG = sreg; }
#define bit_false_atomic(x,mask) {uint8_t sreg = SREG; cli(); (x) &= ~(mask); SREG = sreg; }
#define bit_toggle_atomic(x,mask) {uint8_t sreg = SREG; cli(); (x) ^= (mask); SREG = sreg; }
修改成下面这样,把里面涉及到寄存器的部分给删掉,删掉之后用起来好像也没什么问题。
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
#define bit_true_atomic(x,mask) {uint8_t sreg = SREG; cli(); (x) |= (mask); SREG = sreg; }
#define bit_false_atomic(x,mask) {uint8_t sreg = SREG; cli(); (x) &= ~(mask); SREG = sreg; }
#define bit_toggle_atomic(x,mask) {uint8_t sreg = SREG; cli(); (x) ^= (mask); SREG = sreg; }
#else
#define bit_true_atomic(x,mask) {(x) |= (mask);}
#define bit_false_atomic(x,mask) {(x) &= ~(mask);}
#define bit_toggle_atomic(x,mask) {(x) ^= (mask);}
#endif
3.14、grbl.h文件修改
这个文件里包含了一些没有且用不到的头文件,直接删除或屏蔽即可。并且将所有能用到的头文件全部添加到此文件中,集中管理。
#ifndef grbl_h
#define grbl_h
// Grbl versioning system
#define GRBL_VERSION "0.9j"
#define GRBL_VERSION_BUILD "20160726"
#include "Arduino.h"
#include "EEPROM.h"
// Define standard libraries used by Grbl.
#if (defined CPU_MAP_ATMEGA328P || defined CPU_MAP_ATMEGA2560)
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <util/delay.h>
#endif
#include <math.h>
#include <inttypes.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
// Define the Grbl system include files. NOTE: Do not alter organization.
#include "config.h"
#include "nuts_bolts.h"
#include "settings.h"
#include "system.h"
#include "defaults.h"
#include "cpu_map.h"
#include "coolant_control.h"
#include "eeprom_grbl.h"
#include "gcode.h"
#include "limits_grbl.h"
#include "motion_control.h"
#include "planner.h"
#include "print.h"
#include "probe.h"
#include "protocol.h"
#include "report.h"
#include "serial.h"
#include "spindle_control.h"
#include "stepper.h"
#endif
3.15、config.h文件修改
3.16、main.cpp文件修改
把下面这行定义屏蔽掉。
// #define HOMING_INIT_LOCK
把下面这行定义取消屏蔽(默认是屏蔽的),那么就会以Corexy结构控制电机转动。这个根据实际使用情况来。
#define COREXY
#include "grbl.h"
system_t sys;
void setup() {
// Initialize system upon power-up.
serial_init(); // Setup serial baud rate and interrupts
eeprom_init(); //EEPROM init
settings_init(); // Load Grbl settings from EEPROM
stepper_init(); // Configure stepper pins and interrupt timers
grbl_system_init(); // Configure pinout pins and pin-change interrupt
memset(&sys, 0, sizeof(system_t)); // Clear all system variables
sys.abort = true; // Set abort to complete initialization
sei(); // Enable interrupts
// Check for power-up and set system alarm if homing is enabled to force homing cycle
// by setting Grbl's alarm state. Alarm locks out all g-code commands, including the
// startup scripts, but allows access to settings and internal commands. Only a homing
// cycle '$H' or kill alarm locks '$X' will disable the alarm.
// NOTE: The startup script will run after successful completion of the homing cycle, but
// not after disabling the alarm locks. Prevents motion startup blocks from crashing into
// things uncontrollably. Very bad.
#ifdef HOMING_INIT_LOCK
if (bit_istrue(settings.flags,BITFLAG_HOMING_ENABLE)) { sys.state = STATE_ALARM; }
#endif
// Force Grbl into an ALARM state upon a power-cycle or hard reset.
#ifdef FORCE_INITIALIZATION_ALARM
sys.state = STATE_ALARM;
#endif
// Grbl initialization loop upon power-up or a system abort. For the latter, all processes
// will return to this loop to be cleanly re-initialized.
for(;;)
{
// TODO: Separate configure task that require interrupts to be disabled, especially upon
// a system abort and ensuring any active interrupts are cleanly reset.
// Reset Grbl primary systems.
serial_reset_read_buffer(); // Clear serial read buffer
gc_init(); // Set g-code parser to default state
spindle_init();
coolant_init();
limits_init();
probe_init();
plan_reset(); // Clear block buffer and planner variables
st_reset(); // Clear stepper subsystem variables.
// Sync cleared gcode and planner positions to current system position.
plan_sync_position();
gc_sync_position();
// Reset system variables.
sys.abort = false;
sys_rt_exec_state = 0;
sys_rt_exec_alarm = 0;
sys.suspend = false;
sys.soft_limit = false;
// Start Grbl main loop. Processes program inputs and executes them.
protocol_main_loop();
}
}
void loop() {
// put your main code here, to run repeatedly:
}
整到这里,基本上就改完了,可能会有一些小的地方没有叙述到,比如包含了没有的头文件,那这种的直接删掉就行,就不说了。后面会上传一份修改好的可用的代码。
3.17、调转X、Y、Z轴移动方向
方法一:
打开gcode.cpp文件,搜索case ‘Y’,会看到这是一个switch分支语句中的一个分支,上下还有case 'Y’和case ‘Z’,如下。
case 'X': word_bit = WORD_X; gc_block.values.xyz[X_AXIS] = value; axis_words |= (1<<X_AXIS); break;
case 'Y': word_bit = WORD_Y; gc_block.values.xyz[Y_AXIS] = value; axis_words |= (1<<Y_AXIS); break;
case 'Z': word_bit = WORD_Z; gc_block.values.xyz[Z_AXIS] = value; axis_words |= (1<<Z_AXIS); break;
分析一波可以发现接收到的包含了XYZ轴位置数据的命令最后都会执行到这里,并且将位置数据赋值给gc_block.values.xyz数组。所以,如果我想调转X轴的方向,完全可以在这个case 'X’分支中进行修改,将value这个代表位置数据的变量的正负调转一下,即改成"-value",那么原本是接收到的是-10就变成了10,5就变成了-5,这方向直接就调转过来了简单明了。比如想调转一下Y轴的方向,那么修改成如下即可。
case 'Y': word_bit = WORD_Y; gc_block.values.xyz[Y_AXIS] = (0 - value); axis_words |= (1<<Y_AXIS); break;
就是把value这个变量取反了,正变负,负变正。修改其他轴也是这样,很简单,但每次修改都需要重新烧录程序。
方法二:
通过修改 “$3” 参数实现方向调转。$3对应一个8位的数据,最低位用于修改X轴方向,次低位用于修改Y轴方向,次次低位用于修改Z轴方向。默认情况$3参数为0,即每一位都是0,如果只修改X轴的方向,那么只需要将最低位设置为1,即00000001对应十进制为1,对应发送$3=1命令;如果只修改Y轴方向,那么只需要将次低位设置为1,00000010,对应十进制为2,对应发送$3=2命令;如果XY轴方向都修改,那么最低位和次低位都设置为1,即00000011,对应的十进制为3,对应发送$3=3命令。
有的上位机可以设置生成数据的方向坐标,就算机器的某一轴方向反了,在上位机中设置一下那个轴,生成的数据也会按设置的方向生成数值,还是挺方便的。有的行,有的不行。
4、硬件结构
这是自己做的机器整体结构图,Corexy结构,结构布局参考了稚晖君的X-bot,PCB,三维结构都是自己绘制的。
驱动芯片用的TMC2209,黄鱼上收的价格也很合适,画的PCB刚好能安装到电机底部。主芯片用的的ESP32,有蓝牙和wifi,还能方便扩展,主要也便宜,就是体积大了点。本来打算用32U的,这个型号没有板载天线,用外置ipex天线,这样芯片就能全部放到CPB主板上面了。但是快递到的晚了,就用手里有的了,就天线那部分多出来点,其实问题也不大。
这种结构吧,我觉得就适合做小点,如果Y轴的光轴过长,并且前面的夹笔结构伸出去的较多,前面可能就会下沉,甚至抬笔状态下笔和纸也是挨着的。总体来说还是比较小巧的,如果用蓝牙或wifi传输数据的话,只需要插上电源线就行,电机走线都梳理过,也算整洁。但是这种结构移动的时候带着中间的两个电机一起动,感觉不是很好。要真想抄笔记用,还是得用铝材搭架子,像大鱼做的那种,稳的一P。自己这个反正也是做着玩的,问题不大。
5、上机试验
设置好脉冲输出引脚和方向控制引脚,使能引脚,PWM输出引脚,编译好烧录程序就OK了,根据实际情况设置一些关键的参数,使用串口助手发送"$$“字符可以看到相关参数,如下图。
$100、$101、$102是设置XYZ移动脉冲步数的,多少脉冲走1mm,应该是这意思。这个和驱动器的细分也有关系,细分设置不同数值也要改变。要是不知道多少,就随便设吧,能动就行。不知道为啥,$110、$111这两个速度参数都整到了70000才快起来,我看别人好像也就7000、8000吧,好离谱呀,在移植程序的过程发现在定时器那部分有个和CPU的主频相关联的参数,ESP32是240M,要是把那个参数改小点,比如80M,这个速度数值都需要整这么大,要是20M,那可就快了,10mm瞬时完成,把同步带都扯松了。感觉这个值不一定就非得和CPU主频一样,我看网上有个开源的程序翻了一下发现设置的是20M,这样的话速度值不用设置这么大了。
使用串口助手发送"G91 X10”,假设当前位置为0,那么会移动到XY坐标的(10,0)位置,和直角坐标系一样,记得单位是毫米。如果发送"G91 X10Y-10",假设当前位置为0,那么会移动到XY坐标的(10,-10)位置。如果发送的是X10,但是却向设定的X反方向移动了10mm,就是实际和设定的方向是相反的,这种情况通过改变步进电机和驱动器的接线顺序解决,也可以直接在程序中修改。这里就说如果修改程序了,毕竟程序在手,想怎么改就怎么改。这格修改办法在3.17节,这里就不叙述了。
下面是使用微雕管家生成数据,绘制矢量字体。
因为抬笔那部分使用弹簧压的,抬笔动作快,而放笔的时候因为弹簧压力小有些慢,笔还没接触过到纸呢,就已经移动了从而导致有些地方有没有很好的连接起来。而且前方的抬笔结构用的直线轴承和光轴,但是这个轴承和光轴之间间隙有点大,导致笔前后左右都能晃动,绘制起来受力不均也会出现误差。同步末端是夹到抬笔结构两侧的,从结构图中可以看出来,这个没有使用螺丝上紧,导致工作一段时间,就需要手动上紧同步带,不然太松散也会有误差。整体来说还有很大的优化空间,就效果来说还算可以。
6、总结语
本次主要是介绍GRBL的移植过程,虽说是介绍,但说的也不清楚,具体的可以对照代码看,看一下GRBL的工作流程,了解之后也方便进行二次开发,后面会上传一份完整的项目工程。为了本次博客记录,专门又新建的工程,又从头整了一边,这下可算是记忆优新呀。
回想到最开始接触GRBL,那会忘了可以先找个可以编译成功的工程学习,就逮着源代码在那看,从Github上下载源代码后看到那么多行,看的我是一阵头大感觉无从下手,那真是硬着头皮看呀,跳转到看着比较关键的函数,最后移植起来发现也还好,毕竟修改的都是涉及到单片机外设部分,核心的算法部分反正不需要自己担心,毕竟都是C语言。
因为是应用到写字机器人上,所以有些用不到的功能没有进行适配,直接屏蔽或删除了,感觉只要不是做CNC,估计那些都用不到,最主要的还是XYZ三轴电机的控制部分。
实现了GRBL,那么就可以应用到以步进电机为动力源的项目中,当然只有3轴,更多轴的话可能就需要自己加工了。使用这个可以省了步进电机控制算法的研究,运动策略的研究,插补算法了等等,毕竟更新这么些版本了,算法还是很成熟的呀。可以将其应用到三轴机械臂上,本来还打算整一下的,但是没啥时间了,就放弃了。
说实话这个东西最后是作为我的毕业设计的,为此还特地完善了一下,就是上面摆放的实物图,已经是第二版的结果了。最开始做的时候兴致满满,当第一版整完后就没什么动力了,放那了也没完善。后来因为毕业设计不知道做啥,就又把这个东西拎出来了,只做写字机器人感觉内容不是很多,对此还整了一个带触摸的液晶屏控制板,实现SD卡数据读取,离线调试,触摸屏绘制等功能。写这个博客的时候毕业设计已经接近尾声了,要毕业了,世界上又多了一个打工人。
后面会将GRBL源代码和移植好的工程文件上传CSDN,设置的5积分,我想试试有人下载的话这积分是会到我这吗,等过段时间我就设置成免费下载。如果你没有积分,可以在下面留言。
如果我哪里没说清楚,还有什么问题,可以留言,这个会通过邮箱提示我,我看到会回复的。
以上纯属个人经验,并且个人能力也有限,可能出现差错,如有问题请指正,我会修改的。
项目工程下载地址:https://download.csdn.net/download/wojueburenshu/85603352