# 串口

grbl使用串口从上位机接收信息,并通过串口反馈给上位机。共分为以下几部分:1.串口的配置,如波特率、停止位、校验位等;2.使用环形队列 作为缓冲器,用以匹配上位机和单片机的速度差异,分为输入缓冲器和输出缓冲器; 3.串口数据的简单分配,即分为实时响应和普通响应(放入队列)。

# 串口配置

  1. # 串口初始化

// 串口初始化
void serial_init()
{
  // 设置波特率
  #if BAUD_RATE < 57600
    uint16_t UBRR0_value = ((F_CPU / (8L * BAUD_RATE)) - 1)/2 ;
    UCSR0A &= ~(1 << U2X0); // 关闭波特率倍增器。 - 只在Uno xxx上需要。
  #else
    uint16_t UBRR0_value = ((F_CPU / (4L * BAUD_RATE)) - 1)/2;
    UCSR0A |= (1 << U2X0);  // 波特率高的波特率倍增器开启,即115200
  #endif
  // 波特率是比较大的数字,需要两个8位寄存器存放
  UBRR0H = UBRR0_value >> 8; // 高8位右移到低8位,放入高8位寄存器,右移不会改变源数值
  UBRR0L = UBRR0_value; // 第八位直接放入低8位寄存器

  // 启用接收,发送和接收完成一个字节的中断
  UCSR0B |= (1<<RXEN0 | 1<<TXEN0 | 1<<RXCIE0);

  // 默认协议是8位,无奇偶校验,1个停止位
}

代码解析:

UBRR0(串口波特率寄存器): 这是一个16位的寄存器,需要两次分别传入一个高字节UBRR0H和低字节UBRR0L,根据公式UBBR0=(F_CPU/(4*BAUD_RATE)-1)/2,F_CPU设置为16000000(跟硬件一致),BAUD_RATE配置为115200,得出结果为16.36111111111111取整后得到16,跟手册Table19-12中的波特率为115200时的值16一致(注1)。 UCSR0A(串口控制及状态寄存器A): 在高波特率时(>57600)使能的波特率倍增器U2X0以减少误差,但是为了保证在较老的Arduino设备上禁用波特率倍增器用以兼容它们的bootloader。

UCSR0B(串口控制及状态寄存器B): 使能串口接受RXEN0和串口发送功能TXEN0, 并使能串口接受完成中断RXCIE0,串口发送中断只在需要时开启。

UCSR0C(串口控制及状态寄存器C): UMSEL0(串口模式选择位),默认为00即异步串口, UPMSEL0(串口校验模式选择位),默认为00即默认无奇偶校验。USBS0(串口停止位模式选择位)默认为0即1停止位。UCSZ0(串口字符长度寄存器),默认为011即8位字符。这些都是默认值,不需要手动再配置。

  1. # 串口中断处理:

串口接收中断:

// 串口数据接收中断处理
ISR(SERIAL_RX)
{
  uint8_t data = UDR0; // 从串口数据寄存器取出数据
  uint8_t next_head; // 初始化下一个头指针

  // 直接从串行流中选取实时命令字符。这些字符不被传递到主缓冲区,但是它们设置了实时执行的系统状态标志位。
  switch (data) {
    case CMD_RESET:         mc_reset(); break; // 调用运动控制重置程序
    case CMD_STATUS_REPORT: system_set_exec_state_flag(EXEC_STATUS_REPORT); break; // 状态报告
    case CMD_CYCLE_START:   system_set_exec_state_flag(EXEC_CYCLE_START); break; // 循环开始
    case CMD_FEED_HOLD:     system_set_exec_state_flag(EXEC_FEED_HOLD); break; // 进给保持
    default :
      if (data > 0x7F) { // 实时控制都是扩展的ASCII字符
        switch(data) {
          case CMD_SAFETY_DOOR:   system_set_exec_state_flag(EXEC_SAFETY_DOOR); break; // 设置为 true
          case CMD_JOG_CANCEL:   
            if (sys.state & STATE_JOG) { // 阻止所有其他状态,调用运动取消。
              system_set_exec_state_flag(EXEC_MOTION_CANCEL); 
            }
            break; 
          #ifdef DEBUG
            case CMD_DEBUG_REPORT: {uint8_t sreg = SREG; cli(); bit_true(sys_rt_exec_debug,EXEC_DEBUG_REPORT); SREG = sreg;} break;
          #endif
          // 以下为实时覆盖命令
          case CMD_FEED_OVR_RESET: system_set_exec_motion_override_flag(EXEC_FEED_OVR_RESET); break;
          case CMD_FEED_OVR_COARSE_PLUS: system_set_exec_motion_override_flag(EXEC_FEED_OVR_COARSE_PLUS); break;
          case CMD_FEED_OVR_COARSE_MINUS: system_set_exec_motion_override_flag(EXEC_FEED_OVR_COARSE_MINUS); break;
          case CMD_FEED_OVR_FINE_PLUS: system_set_exec_motion_override_flag(EXEC_FEED_OVR_FINE_PLUS); break;
          case CMD_FEED_OVR_FINE_MINUS: system_set_exec_motion_override_flag(EXEC_FEED_OVR_FINE_MINUS); break;
          case CMD_RAPID_OVR_RESET: system_set_exec_motion_override_flag(EXEC_RAPID_OVR_RESET); break;
          case CMD_RAPID_OVR_MEDIUM: system_set_exec_motion_override_flag(EXEC_RAPID_OVR_MEDIUM); break;
          case CMD_RAPID_OVR_LOW: system_set_exec_motion_override_flag(EXEC_RAPID_OVR_LOW); break;
          case CMD_SPINDLE_OVR_RESET: system_set_exec_accessory_override_flag(EXEC_SPINDLE_OVR_RESET); break;
          case CMD_SPINDLE_OVR_COARSE_PLUS: system_set_exec_accessory_override_flag(EXEC_SPINDLE_OVR_COARSE_PLUS); break;
          case CMD_SPINDLE_OVR_COARSE_MINUS: system_set_exec_accessory_override_flag(EXEC_SPINDLE_OVR_COARSE_MINUS); break;
          case CMD_SPINDLE_OVR_FINE_PLUS: system_set_exec_accessory_override_flag(EXEC_SPINDLE_OVR_FINE_PLUS); break;
          case CMD_SPINDLE_OVR_FINE_MINUS: system_set_exec_accessory_override_flag(EXEC_SPINDLE_OVR_FINE_MINUS); break;
          case CMD_SPINDLE_OVR_STOP: system_set_exec_accessory_override_flag(EXEC_SPINDLE_OVR_STOP); break;
          case CMD_COOLANT_FLOOD_OVR_TOGGLE: system_set_exec_accessory_override_flag(EXEC_COOLANT_FLOOD_OVR_TOGGLE); break;
          #ifdef ENABLE_M7
            case CMD_COOLANT_MIST_OVR_TOGGLE: system_set_exec_accessory_override_flag(EXEC_COOLANT_MIST_OVR_TOGGLE); break;
          #endif
        }
        // 除了上面已知的实时命令,其他的ASCII扩展字符都被丢掉
      } else { // 其他的字符被认为都是G代码,会被写入到主缓冲区
        next_head = serial_rx_buffer_head + 1; // 更新临时头指针
        if (next_head == RX_RING_BUFFER) { next_head = 0; }

        // 写入到接收缓冲区,直到它满了为止。
        if (next_head != serial_rx_buffer_tail) {
          serial_rx_buffer[serial_rx_buffer_head] = data;
          serial_rx_buffer_head = next_head;
        }
      }
  }
}

代码解析:

AVR并没有实现中断功能,中断实现是由编译器gcc-avr完成的,具体用法是在中断函数前用__attribute__((interrupt))修饰,ISR(SERIAL_RX)是一个宏定义,它在interrupt.h中定义,功能是根据传入的中断向量号生成中断函数定义和函数声明。

开启了中断并设置了中断函数,一旦串口中接收到了一个字节数据,就会触发中断,从UDR0串口数据寄存器中取出数据后,会做简单区分,这里有三种类型的数据:

  1. 实时命令,不会放入串口接收队列。
  2. 实时覆盖命令,能实时调整部分参数,也不会放入串口接收队列。
  3. 正常的G代码和系统命令,会放入串口接收队列。

串口发送中断:

// 数据寄存器为空的中断处理
ISR(SERIAL_UDRE)
{
  // 由于环形队列尾指针中断和主程序都会使用,有可能导致数据读取时,指针已经发生了变化,
  // 存在不稳定性,所以要用临时变量暂存,增加读取时的稳定性。
  uint8_t tail = serial_tx_buffer_tail; // 临时变量暂存 serial_tx_buffer_tail (为volatile优化)

  // 从缓冲区发送一个字节到串口
  UDR0 = serial_tx_buffer[tail];

  // 更新尾指针位置,如果已经到达顶端,返回初始位置,形成环形
  tail++;
  if (tail == TX_RING_BUFFER) { tail = 0; }

  serial_tx_buffer_tail = tail;

  // 如果环形队列为空,关闭串口数据寄存器为空的中断,阻止继续发送串口流
  if (tail == serial_tx_buffer_head) { UCSR0B &= ~(1 << UDRIE0); }
}

代码解析:

ISR(SERIAL_UDRE)也是一个宏定义,展开后是根据串口数据为空的中断号定义的中断处理函数,发送中断的开启是在有数据需要返回给上位机时(如serial_write函数被调用时)使能UDRIE0(串口数据寄存器为空中断使能位)实现的的,UCSR0B |= (1 << UDRIE0);,开启中断后如果数据发送寄存器为空会立即触发中断。随后把发送队列的数据放到UDR0(串口数据寄存器)发送给上位机。当串口发送队列没有数据时需要禁用串口发送中断,以防止误触发中断。需要注意的是串口接收和发送都是用的UDR0寄存器,这样不会导致接收发送冲突吗?不会,UDR0的接收和发送只是共享了寄存器地址,读和写是分离在不同的硬件上实现的。

# 环形队列

环形队列是在实际编程极为有用的数据结构,它是一个首尾相连的FIFO的数据结构,采用数组的线性空间,数据组织简单。能很快知道队列是否满为空。能以很快速度的来存取数据。

  1. 缓冲:使用队列可以缓冲数据,提升收发数据的性能。

  2. 高效:相比直线队列,空间利用率高。

  3. 多任务:配合中断,串口和主循环可以在互不干扰的情况下独立工作。

grbl中的环形队列使用数组实现,使用两个指针标记队头队尾(不过grbl这里是反的),通过保持一个数据单元为空策略判断队列满和空。我制作了一个演示程序,想不明白的可以实操试一试更容易理解。

# 串口接收环形队列

#ifndef RX_BUFFER_SIZE
  #define RX_BUFFER_SIZE 128
#endif
#define RX_RING_BUFFER (RX_BUFFER_SIZE+1) // 定义接收缓冲区环形队列长度
uint8_t serial_rx_buffer[RX_RING_BUFFER]; // 定义串口接收环形队列
uint8_t serial_rx_buffer_head = 0; // 定义串口接收环形队列头指针
volatile uint8_t serial_rx_buffer_tail = 0; // 定义串口接收环形队列尾指针

定义了一个RX_BUFFER_SIZE大小(128字节)的串口接收环形队列serial_rx_buffer,并使用了队头serial_rx_buffer_head和队尾serial_rx_buffer_tail两个指针记录队列状态。

// 获取串口接收缓冲区的第一个字节。被主程序调用。
uint8_t serial_read()
{
  uint8_t tail = serial_rx_buffer_tail; // 临时变量暂存 serial_rx_buffer_tail (优化volatile)
  if (serial_rx_buffer_head == tail) { // 如果接收环形队列为空,则设置结束符号
    return SERIAL_NO_DATA;
  } else {
    uint8_t data = serial_rx_buffer[tail]; // 从接受环形队列取一个字节

    tail++; // 更新尾指针
    if (tail == RX_RING_BUFFER) { tail = 0; } // 环形
    serial_rx_buffer_tail = tail;

    return data;
  }
}

serial_read一个读取串口环形队列的接口:这个接口在主循环中调用,它从串口接收一个字节就更新一下队尾的指针,因为使用的是数组,指针到达数组尾部要返回数组头部形成环形,如果队列是空的serial_rx_buffer_head == tail,就返回结束符号0xff

# 串口发送环形队列

#define TX_BUFFER_SIZE 104 // 定义串口发送缓冲区大小
#define TX_RING_BUFFER (TX_BUFFER_SIZE+1) // 定义发送缓冲区队列长度

int8_t serial_tx_buffer[TX_RING_BUFFER]; // 定义串口发送环形队列
uint8_t serial_tx_buffer_head = 0; // 定义串口发送环形队列头指针
volatile uint8_t serial_tx_buffer_tail = 0; // 定义串口发送环形队列尾指针

定义了一个大小为TX_BUFFER_SIZE(104)的串口发送环形队列serial_tx_buffer,并使用了队头serial_tx_buffer_head和队尾serial_tx_buffer_tail两个指针记录队列状态。

// 写入一个字节到串口发送缓冲区。被主程序调用。
void serial_write(uint8_t data) {
  // 计算下一个头指针,如果已经到达最大值,移到开始,形成环形
  uint8_t next_head = serial_tx_buffer_head + 1;
  if (next_head == TX_RING_BUFFER) { next_head = 0; }

  // 等待,直到缓冲区有空间
  while (next_head == serial_tx_buffer_tail) {
    // 代办:重构st_prep_tx_buffer()调用,在长打印期间在这里执行。
    if (sys_rt_exec_state & EXEC_RESET) { return; } // 只检查终止防止死循环。
  }

  // 储存数据并向前移动头指针
  serial_tx_buffer[serial_tx_buffer_head] = data;
  serial_tx_buffer_head = next_head;

  // 开启数据寄存器为空的中断,确保串口发送流运行。
  // 只要环形队列有空间,就可以持续不断地从串口接收数据。
  UCSR0B |=  (1 << UDRIE0);
}

serial_write一个串口写入接口,这个接口主要被反馈报告程序调用,报告程序把字符串格式化之后传入这个接口,随后把传进来的数据放入发送队列,如果队列满了就一直等着,直到队列数据被串口取出留出空间,最后开启串口数据寄存器为空的中断,开启串口发送处理中断,由ISR(SERIAL_UDRE)将队列中的数据发送给上位机。

# 辅助函数

// 返回串口读缓冲区可用字节数。
uint8_t serial_get_rx_buffer_available()
{
  uint8_t rtail = serial_rx_buffer_tail; // 临时变量暂存尾指针优化volatile
  if (serial_rx_buffer_head >= rtail) { return(RX_BUFFER_SIZE - (serial_rx_buffer_head-rtail)); }
  return((rtail-serial_rx_buffer_head-1));
}

serial_get_rx_buffer_available一个用以返回串口接收环形队列空闲字节数的接口,它会被反馈报告程序调用。它采用镜像法把队列尺寸扩大2倍,虚拟出另一个队列,方便计算。

// 返回串口读缓冲区已用的字节数。
// 注意:已废弃。不再被使用除非在config.h中开启了经典状态报告。
uint8_t serial_get_rx_buffer_count()
{
  uint8_t rtail = serial_rx_buffer_tail; // 临时变量暂存尾指针优化volatile
  if (serial_rx_buffer_head >= rtail) { return(serial_rx_buffer_head-rtail); }
  return (RX_BUFFER_SIZE - (rtail-serial_rx_buffer_head));
}

返回串口接收队列已用的字节数。计算方法与上面类似。

// 返回串口发送缓冲区已用的字节数。
// 注意:没有用到除非为了调试和保证串口发送缓冲区没有瓶颈。
uint8_t serial_get_tx_buffer_count()
{
  uint8_t ttail = serial_tx_buffer_tail; // Copy to limit multiple calls to volatile
  if (serial_tx_buffer_head >= ttail) { return(serial_tx_buffer_head-ttail); }
  return (TX_RING_BUFFER - (ttail-serial_tx_buffer_head));
}

返回串口发送队列已用的字节数。计算方法与上面类似。