移植完步进电机驱动, 但是发现执行代码 G1 X10 Y10 F1000\n时,偶尔会卡住. 无奈只好全流程的从前往后分析整个系统的执行流程. 这篇文章算是核心, 对整个grbl进行了系统性的分析. 看完这篇文章, 基本上对整个系统做了哪些事情, 经过哪些文件和函数有个大致的了解.
G代码是如何从电脑到执行的? 中间经过了那几个代码文件和函数?又经过了哪些变量?
为了容易理解后来我又加了一张图. 方便排查问题
- 首先是serial.c串口RX中断函数将数据读取到 rxbuffer
- 将 stream.c的函数指向 serialGetC()从而读取 rxbuffer的数据
- protocol.c 的 protocol_main_loop主循环中循环读取G代码.
while((c = hal.stream.read()) != SERIAL_NO_DATA) {
....
})
读取到每个字符, 然后开始解析G代码. 当读取到 ‘\0’ 的时候开始
将读取到的代码读取到 xcommand变量, 然后执行 gcode.c中的 gc_execute_block(xcommand)
4. gcode.c 解析G代码.字符串转变量
gcode.c中这个函数主要的功能是将G-Code的字符串代码转换成可以使用的内存变量.
// Handle extra command (internal stream)
if(xcommand[0] != '\0') {
if (xcommand[0] == '$') // Grbl '$' system command
system_execute_line(xcommand);
else if (state_get() & (STATE_ALARM|STATE_ESTOP|STATE_JOG)) // Everything else is gcode. Block if in alarm, eStop or jog state.
grbl.report.status_message(Status_SystemGClock);
else // Parse and execute g-code block.
gc_execute_block(xcommand);
xcommand[0] = '\0';
}
- motion_control.c 根据变量实现运动画圈,画线等功能
然后在解析G代码的时候调用 motion_control.c中的 mc_line(),mc_arc(),mc_dwell(),等函数. 实现运动功能 .
// Execute linear motion in absolute millimeter coordinates. Feed rate given in millimeters/second
// unless invert_feed_rate is true. Then the feed_rate means that the motion should be completed in
// (1 minute)/feed_rate time.
// NOTE: This is the primary gateway to the grbl planner. All line motions, including arc line
// segments, must pass through this routine before being passed to the planner. The separation of
// mc_line and plan_buffer_line is done primarily to place non-planner-type functions from being
// in the planner and to let backlash compensation or canned cycle integration simple and direct.
// 在绝对毫米坐标中执行线性运动。进给速度以毫米/秒为单位
// 除非 invert_feed_rate 为 true。否则,feed_rate 表示运动应在(1分钟)/进给速度时间。
// 注意:这是grbl规划器的主要入口。所有直线运动,包括弧线
// 在传递给规划器之前,必须通过这个例程mc_line 和 plan_buffer_line 的主要目的是放置非规划器类型的函数,以免
// 在计划器中,可以简单直接地进行间隙补偿或屏蔽循环集成。
bool mc_line (float *target, plan_line_data_t *pl_data)
{
...
}
motion_control.c中调用 plan_buffer_line() 函数, 将运动操作添加到运动规划中.
- planner.c plan_buffer_line() 函数运动规划
/* Add a new linear movement to the buffer. target[N_AXIS] is the signed, absolute target position
in millimeters. Feed rate specifies the speed of the motion. If feed rate is inverted, the feed
rate is taken to mean "frequency" and would complete the operation in 1/feed_rate minutes.
All position data passed to the planner must be in terms of machine position to keep the planner
independent of any coordinate system changes and offsets, which are handled by the g-code parser.
NOTE: Assumes buffer is available. Buffer checks are handled at a higher level by motion_control.
In other words, the buffer head is never equal to the buffer tail. Also the feed rate input value
is used in three ways: as a normal feed rate if invert_feed_rate is false, as inverse time if
invert_feed_rate is true, or as seek/rapids rate if the feed_rate value is negative (and
invert_feed_rate always false).
The system motion condition tells the planner to plan a motion in the always unused block buffer
head. It avoids changing the planner state and preserves the buffer to ensure subsequent gcode
motions are still planned correctly, while the stepper module only points to the block buffer head
to execute the special system motion. */
// 将新的线性移动添加到缓冲区。target[N_AXIS]是有符号的绝对目标位置
// 单位为毫米。进给速度指定了运动的速度。如果进给率反转,进给速率是指“频率”,将在1/进给速率分钟内完成操作。
// 所有传递给计划器的位置数据必须以机器位置为准,以保持计划器的准确性
// 独立于任何坐标系变化和偏移,这些由g代码解析器处理。
// 注意:假设缓冲区可用。缓冲区的检查由motion_control在更高的层次上处理。
// 换句话说,缓冲区头永远不会等于缓冲区尾。此外,进给率输入值
// 有三种使用方式:如果 invert_feed_rate 为 false,则作为正常进给率;如果 invert_feed_rate 为 true,则作为反时;如果 invert_feed_rate 为 false,则作为反时
// 如果 invert_feed_rate 为真,或者如果 feed_rate 的值为负(并且
// 逆进给速度始终为假)。
// 系统运动条件告诉计划器在始终未使用的块缓冲区中计划一个运动
// 头它避免了改变计划器状态并保留缓冲区以确保后续的gcode
// 运动仍然正确计划,而步进器模块仅指向块缓冲区头
// 以执行特殊系统运动。
bool plan_buffer_line (float *target, plan_line_data_t *pl_data)
{
}
- protocol.c protocol_auto_cycle_start()
上面只是解析命令. 并未真正执行, 真正的执行是在.
这些数据都处理好之后,protocol.c文件会在 gc_execute_block(xcommand); 之后直接执行
protocol_auto_cycle_start()函数. 这个函数会通过调用plan_get_current_block() 读取前面解析生产的运动规划数据. 然后调用 system_set_exec_state_flag(EXEC_CYCLE_START);函数.
中也只是修改了一个变量Flag.
// Auto-cycle start triggers when there is a motion ready to execute and if the main program is not
// actively parsing commands.
// NOTE: This function is called from the main loop, buffer sync, and mc_line() only and executes
// when one of these conditions exist respectively: There are no more blocks sent (i.e. streaming
// is finished, single commands), a command that needs to wait for the motions in the buffer to
// execute calls a buffer sync, or the planner buffer is full and ready to go.
//当有动作准备执行时,如果主程序未执行,则自动循环启动会触发
//主动解析命令。
//注意:此函数仅从主循环、缓冲区同步和mc_line()调用并执行
//当分别存在以下条件之一时:不再发送块(即流式传输
//已完成,单个命令),该命令需要等待缓冲区中的运动
//execute调用缓冲区同步,或者计划缓冲区已满并准备就绪。
void protocol_auto_cycle_start (void)
{
if (plan_get_current_block() != NULL) // Check if there are any blocks in the buffer.
system_set_exec_state_flag(EXEC_CYCLE_START); // If so, execute them!
}
- system_set_exec_state_flag() 这个函数是个宏定义只是设置了一个状态. 并未执行
#define system_set_exec_state_flag(mask) hal.set_bits_atomic(&sys.rt_exec_state, (mask))
那么这些plan.c规划好的命令是在什么地方被执行的呢?
经过分析这些plan 既然被保存,肯定会被使用, 只要搜索在什么地方使用它的函数即可.
plan.c中有个函数是plan_get_current_block , 这个函数明显的是给外部使用的接口.
// Returns address of first planner block, if available. Called by various main program functions.
plan_block_t *plan_get_current_block (void)
{
return block_buffer_head == block_buffer_tail ? NULL : block_buffer_tail;
}
- state_machine.c 状态机循环执行plan
经过一番搜索plan_get_current_block . 发现是在 state_machine.c中执行的.
在 void state_set (sys_state_t new_state) 函数中不停的进行状态切换, 其中当状态为STATE_CYCLE时进行读取. 事实并非如此简单 . 还有另外一个循环. 状态机是有主循环调用的. 但是步进电机的输出还有另外一个循环.后面再说, 先看代码.
void state_set (sys_state_t new_state)
{
if (new_state != sys_state) {
switch(new_state) { // Set up new state and handler
case STATE_IDLE:
sys.suspend = false; // Break suspend state.
sys.step_control.flags = 0; // Restore step control to normal operation.
sys.parking_state = Parking_DoorClosed;
sys.holding_state = Hold_NotHolding;
sys_state = pending_state = new_state;
park.flags.value = 0;
stateHandler = state_idle;
break;
case STATE_CYCLE:
if (sys_state == STATE_IDLE) {
// Start cycle only if queued motions exist in planner buffer and the motion is not canceled.
plan_block_t *block;
if ((block = plan_get_current_block())) {
sys_state = new_state;
sys.steppers_deenergize = false; // Cancel stepper deenergize if pending.
st_prep_buffer(); // Initialize step segment buffer before beginning cycle.
if (block->spindle.state.synchronized) {
if (block->spindle.hal->reset_data)
block->spindle.hal->reset_data();
uint32_t index = block->spindle.hal->get_data(SpindleData_Counters)->index_count + 2;
while(index != block->spindle.hal->get_data(SpindleData_Counters)->index_count); // check for abort in this loop?
}
st_wake_up();
stateHandler = state_cycle;
}
}
break;
case STATE_JOG:
if (sys_state == STATE_TOOL_CHANGE)
pending_state = STATE_TOOL_CHANGE;
sys_state = new_state;
stateHandler = state_cycle;
break;
case STATE_TOOL_CHANGE:
sys_state = new_state;
stateHandler = state_await_toolchanged;
break;
case STATE_HOLD:
if (sys.override.control.sync && sys.override.control.feed_hold_disable)
sys.flags.feed_hold_pending = On;
if (!((sys_state & STATE_JOG) || sys.override.control.feed_hold_disable)) {
if (!initiate_hold(new_state)) {
sys.holding_state = Hold_Complete;
stateHandler = state_await_resume;
}
sys_state = new_state;
sys.flags.feed_hold_pending = Off;
}
break;
case STATE_SAFETY_DOOR:
if ((sys_state & (STATE_ALARM|STATE_ESTOP|STATE_SLEEP|STATE_CHECK_MODE)))
return;
grbl.report.feedback_message(Message_SafetyDoorAjar);
// no break
case STATE_SLEEP:
sys.parking_state = Parking_Retracting;
if (!initiate_hold(new_state)) {
if (pending_state != new_state) {
sys_state = new_state;
state_await_hold(EXEC_CYCLE_COMPLETE); // "Simulate" a cycle stop
}
} else
sys_state = new_state;
break;
case STATE_ALARM:
case STATE_ESTOP:
case STATE_HOMING:
case STATE_CHECK_MODE:
sys_state = new_state;
sys.suspend = false;
stateHandler = state_noop;
break;
}
if (!(sys_state & (STATE_ALARM|STATE_ESTOP)))
sys.alarm = Alarm_None;
if (grbl.on_state_change)
grbl.on_state_change(new_state);
}
}
那么这里有个疑问, 这个状态机是什么时候启动的?
经过分析, 是在 protocol.c的主循环中, protocol_main_loop () 内部循环的给状态state_set()进行赋值.然后调用的 state_set()的时候就进行了代码执行.
-
然后在state_machine.c中装入步进电机的配置并调用步进电机
-
在步进电机驱动层还实现了一个主循环,循环消费plan中的数据
后来经过分析. 发现Stepper模块也是一个自主独立运行的循环. 类似多线程. 由定时器触发中断,中断中循环消费planner.c中st_prep_buffer()给stepper的数据.
在这个st_prep_buffer() 函数里. 会将plan取出放入变量pl_block, 然后又放入st_prep_block变量.
最后拼装成 变量 prep_segment, 将st_prep_block赋值给
prep_segment->exec_block = st_prep_block;
这里prep_segment是个指针指向的是 segment_buffer_head. -
stepper.c 中断函数中进行直线插补算法
然后在步进电机的 中断函数 stepper_driver_interrupt_handler中.
st.exec_block = st.exec_segment->exec_block;
如此, 数据最终到达st.exec_block 变量.
然后就是在 stepper_driver_interrupt_handler 中断函数中. 根据Bresenham算法生成对应的
移动轨迹. Bresenham算法应该是插补算法.
最终生成各个轴对应的输出电平数字信号. 放入 st.step_outbits.value 中.
这个中断函数的触发是由定时器中触发的. .
st.step_outbits.value = step_outbits.value;
然后在stepper_driver_interrupt_handler 中断函数中继续调用 hal.stepper.pulse_start(). 触发driver_stepper.c 中的脉冲输出函数.
hal.stepper.pulse_start(&st);
- driver_stepper.c 执行输出控制芯片引脚
由于hal.stepper.pulse_start 指向的是 stepperPulseStartDelayed 或 stepperPulseStart
hal.stepper.pulse_start()函数最终会调用. set_step_outputs执行电平信号输出.
set_step_outputs(stepper->step_outbits)
至此完. .
总结一下, 大概分以下步.,
第一步 是将字符串转换成变量.
第二步 实现画线,画圆
第三步 运动规划放入buffer.
第四步:状态机中循环取规划,循环执行规划. 并调用步进电机的输出功能,
第五步 步进电机首先使用Bresenham算法进行运动插补.
第六步 调用步进电机.进行输出.