Arduino代码机制-Serial下

来源:互联网 发布:ntoskrnl 占用80端口 编辑:程序博客网 时间:2024/05/19 02:44

真正开始讲串口了。

串口是流的具体实现。串口在类HardwareSerial中描述。类HardwareSerial在Stream的基础上,添加了发送和接收数据缓冲区,实际上是个队列,添加了构造函数和其他实用函数。而我们通常使用的Serial,是HardwareSerial实例化的一个对象。对于不同单片机,他们的串口数目不一样,于是采用预编译的技巧来实例化相应数目的Serial对象。

#if defined(UBRRH) && defined(UBRRL)  HardwareSerial Serial(&UBRRH, &UBRRL, &UCSRA, &UCSRB, &UCSRC, &UDR);#else  HardwareSerial Serial(&UBRR0H, &UBRR0L, &UCSR0A, &UCSR0B, &UCSR0C, &UDR0);#endif

所有型号单片机都至少有一个串口,而如果只有一个串口的话,会将波特率寄存器定义为UBRRH和UBRRL(需要16位来控制波特率,而单片机是8位的,因此定义两个寄存器,分别为高八位和地八位);如果有多个串口的话,则不会将波特率寄存器定义为定义UBRRH、UBRRL,而定义为UBRR0H和UBRR0L。通过这样的区别来给构造函数传入不同参数。

对于串口1,则

#if defined(HAVE_HWSERIAL1)    HardwareSerial Serial1(&UBRR1H, &UBRR1L, &UCSR1A, &UCSR1B, &UCSR1C, &UDR1);#endif

这就比较容易理解了,如果有串口1,就实例化它。对于2,3号串口也是这样。

HardwareSerial

成员变量

这个类的实例通过串口的各个寄存器发送和接收数据,将接收到的数据保存在缓冲区中。缓冲区实际上是个数组队列,要维持这个队列还需要队列头和队列尾。因此成员变量包括以下:

protected:volatile uint8_t * const _ubrrh;volatile uint8_t * const _ubrrl;volatile uint8_t * const _ucsra;volatile uint8_t * const _ucsrb;volatile uint8_t * const _ucsrc;volatile uint8_t * const _udr;//各个控制寄存器volatile rx_buffer_index_t _rx_buffer_head;volatile rx_buffer_index_t _rx_buffer_tail;volatile tx_buffer_index_t _tx_buffer_head;volatile tx_buffer_index_t _tx_buffer_tail;unsigned char _rx_buffer[SERIAL_RX_BUFFER_SIZE];unsigned char _tx_buffer[SERIAL_TX_BUFFER_SIZE];

数据缓冲区大小能根据RAM大小来分配,当RAM小于1000字节时,接收和发送数据缓冲区大小都为16字节,当RAM大于1000字节时,则为64字节。

#if (RAMEND < 1000)#define SERIAL_TX_BUFFER_SIZE 16#define SERIAL_RX_BUFFER_SIZE 16#else#define SERIAL_TX_BUFFER_SIZE 64#define SERIAL_RX_BUFFER_SIZE 64#endif

构造函数

构造函数的参数为各个寄存器的地址,发送和接收数据最底层的实现是通过设置这些寄存器实现的。由于已经为我们实例化了各个串口的相应对象,因此,不需要关心构造函数具体原型。

常用函数

1.串口初始化,设置串口传送波特率,数据格式。默认数据格式为8位数据,无校验位,1位停止位。

void begin(unsigned long baud) { begin(baud, SERIAL_8N1); }void begin(unsigned long baud, uint8_t config);

对于数据格式的设置,有以下定义

#define SERIAL_5N1 0x00#define SERIAL_6N1 0x02#define SERIAL_7N1 0x04#define SERIAL_8N1 0x06#define SERIAL_5N2 0x08#define SERIAL_6N2 0x0A#define SERIAL_7N2 0x0C#define SERIAL_8N2 0x0E#define SERIAL_5E1 0x20#define SERIAL_6E1 0x22#define SERIAL_7E1 0x24#define SERIAL_8E1 0x26#define SERIAL_5E2 0x28#define SERIAL_6E2 0x2A#define SERIAL_7E2 0x2C#define SERIAL_8E2 0x2E#define SERIAL_5O1 0x30#define SERIAL_6O1 0x32#define SERIAL_7O1 0x34#define SERIAL_8O1 0x36#define SERIAL_5O2 0x38#define SERIAL_6O2 0x3A#define SERIAL_7O2 0x3C#define SERIAL_8O2 0x3E

其中,N表示无校验位,E表示偶校验,O表示奇校验。前面一个数字表示发送数据的位数,后面一个数字表示停止位数。

初始化之后就能发送和接收数据了。

2.读取和接收数据
实现了父类Stream中所有虚函数。

int HardwareSerial::available(void){  return ((unsigned int)(SERIAL_RX_BUFFER_SIZE + _rx_buffer_head - _rx_buffer_tail)) % SERIAL_RX_BUFFER_SIZE;}

函数available作用是返回当前可读取字节数。

int HardwareSerial::peek(void){  if (_rx_buffer_head == _rx_buffer_tail) {    return -1;  } else {    return _rx_buffer[_rx_buffer_tail];  }}

函数peek作用是返回当前可读取的数据。如果缓冲区中没有数据,则返回-1,如果有数据则返回当前数据,读取数据后不丢弃。

int HardwareSerial::read(void){  if (_rx_buffer_head == _rx_buffer_tail) {    return -1;  } else {    unsigned char c = _rx_buffer[_rx_buffer_tail];    _rx_buffer_tail = (rx_buffer_index_t)(_rx_buffer_tail + 1) % SERIAL_RX_BUFFER_SIZE;    return c;  }}

函数read作用是返回当前数据并丢弃。如果缓冲区中没有数据,则返回-1,如果有数据则从缓冲区中读取并丢弃。

注意peek和read的区别。

virtual void flush(void);

函数flush作用是将缓冲区中的数据发送完,里面的实现比较底层,就不贴出来了。

对父类Print中的虚函数也给出了实现,Print类中有两个虚函数,其中一个已经在Print中给出了实现,可以不用再这个类中实现,只需实现一个write函数,即发送一个字符。

对于write函数,发送一个字符,应当是把这个字符放到缓冲区里就好了,剩余的部分交给更底层的代码去做。但是为了性能,write函数做了个判断,如果缓冲区里没有数据,则直接将这个数据发送出去。这样能提高数据的传送速度,因为大部分时间里缓冲区是没有数据的,这时候如果将数据写在缓冲区中,那将会等待中断到来才会写。

size_t HardwareSerial::write(uint8_t c){//如果缓冲区没有数据则直接发送数据  if (_tx_buffer_head == _tx_buffer_tail && bit_is_set(*_ucsra, UDRE0)) {    *_udr = c;    sbi(*_ucsra, TXC0);    return 1;  }  tx_buffer_index_t i = (_tx_buffer_head + 1) % SERIAL_TX_BUFFER_SIZE;  while (i == _tx_buffer_tail) {//如果缓冲区满了    if (bit_is_clear(SREG, SREG_I)) {      if(bit_is_set(*_ucsra, UDRE0))        _tx_udr_empty_irq();//如果全局中断屏蔽了,则手动调用发送数据函数    } else {如果中断未屏蔽,则会自动进入中断发送数据    }  }    //将数据写入缓冲区  _tx_buffer[_tx_buffer_head] = c;  _tx_buffer_head = i;  sbi(*_ucsrb, UDRIE0);  _written = true;  return 1;}

底层实现

到这里,出write函数为了性能涉及了底层之外,读取和写入数据都是操作缓冲区的。到底数据是如何从缓冲区到串口发送出去的?又是如何将串口接收到的数据写到缓冲区的呢?

这是通过中断实现的,需要定义两个中断函数,分别用于发送和接收函数。以串口0来说:

#if defined(USART_RX_vect)//用于接收  ISR(USART_RX_vect)#elif defined(USART0_RX_vect)  ISR(USART0_RX_vect)#elif defined(USART_RXC_vect)  ISR(USART_RXC_vect) // ATmega8#endif  {    Serial._rx_complete_irq();  }#if defined(UART0_UDRE_vect)//用于发送ISR(UART0_UDRE_vect)#elif defined(UART_UDRE_vect)ISR(UART_UDRE_vect)#elif defined(USART0_UDRE_vect)ISR(USART0_UDRE_vect)#elif defined(USART_UDRE_vect)ISR(USART_UDRE_vect)#endif{  Serial._tx_udr_empty_irq();}

在这两个中断函数中,分别调用了函数_rx_complete_irq()和_tx_udr_empty_irq(),这两个函数才是操作底层寄存器用来把接收到的数据存入缓冲和从缓冲区中取数据发送出去的。

源代码可以参考源文件:
hardware\arduino\avr\cores\arduino\HardwareSerial.h
hardware\arduino\avr\cores\arduino\hardwareSerial_private.h

0 0
原创粉丝点击