1、前述
??当我在学校的时候,我曾经参加过电子竞赛选择的电机夹笔问题,使用步进电机和导轨,然后,步进电机不太好,最后得到了一个省三,破旧的人。事实上,现在想想,这个话题不需要任何算法,也就是说,细节没有处理好,这将把我放在现在的比较中,高低必须得到一个。 ??比赛结束后,实验室里有很多步进电机,看着新的电机P,想想垃圾的比赛结果,想用这些电机来弥补缺点,和学生讨论做一个写作机器人,也许也可以使用。之后,我研究了步进电机的控制算法,因为比赛结束后几天我就回家了。在家里真的没有效率。我三天钓鱼,两天晒网。我没有精力。因此,这件事一直在拖延,没有进展。后来逛了一个站,看到别人做的写字机器人,不小心得知了GRBL,搜索发现写字机器人可以在其基础上修改,并且可以实现GRBL很成熟,很多版本都更新了。在这里,我想不要自己写算法。我的知识储备太少了。事实上,我像无头苍蝇一样到处碰撞。效率低下,所以我先学会使用它GRBL这个东西,如果用不懂,怎么谈自己做?然后计划移植GRBL写作机器人。 ??后来开学了,在学校做得很好,3D打印零件,虽然不是很好,但基本功能也实现了。移植记录在这里GRBL到ESP32需要修改哪些部分,说不定能还能帮到别人。
文章目录
- 1、前述
- 2、GRBL需要修改的部分
- 3.具体修改过程
-
- 3.1、获取GRBL源代码
- 3.2 添加设置引脚的文件
- 3.3、serial.cpp修改文件相关程序
-
- 3.3.1、serial_init()函数修改
- 3.3.2、修改serial_write()函数程序
- 3.3.3、屏蔽ISR(SERIAL_UDRE)中断回调函数
- 3.3.4、修改ISR(SERIAL_RX)串口中断接收函数
- 3.3.5、修改serial_read()串口数据读取函数
- 3.3、stepper.cpp修改文件相关程序
-
- 3.3.1、修改stepper_init()定时器传输函数
- 3.3.2.修改定时器1中断函数
- 3.3.3.修改定时器2中断函数
- 3.3.4、修改st_wake_up()函数
- 3.3.5、修改st_go_idle()函数
- 3.5.6、修改st_reset()函数
- 3.4、eeprom.cpp修改文件相关程序
-
- 3.4.1、添加eeprom_init()初始化函数
- 3.4.2、修改eeprom_get_char()函数
- 3.4.3、修改eeprom_put_char()函数
- 3.5、spindle_control.cpp修改文件相关程序
-
- 3.5.1、修改spindle_init()主轴初始化函数
- 3.5.2、修改spindle_stop()函数
- 3.5.3、修改spindle_set_state()函数
- 3.6、nuts_bolts.cpp文件先修改程序
-
- 3.6.1、修改delay_ms()延迟函数
- 3.6.2、修改delay_us()延迟函数
- 3.7、probe.cpp修改文件相关程序
-
- 3.7.1 修改probe_init()函数
- 3.7.2、修改probe_get_state()函数
- 3.7.3、修改probe_configure_invert_mask()函数
- 3.8、coolant_control.cpp文件相关程序修改
-
- 3.8.1、修改coolant_init()函数
- 3.8.2、修改coolant_stop()函数
- 3.8.2、修改coolant_set_state()函数
- 3.9、limits.cpp文件相关程序修改
-
- 3.9.1、修改limits_init()函数
- 3.9.2、修改limits_disable()函数
- 3.9.3、修改ISR(LIMIT_INT_vect)函数
- 3.9.4、修改limits_get_state()函数
- 3.10、system.cpp修改文件相关程序
-
- 3.10.1、修改system_init()函数
- 3.10.2、修改ISR(CONTROL_INT_vect) 函数
- 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节中定义了,而和没有被定义,所以会编译对应的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)