ATtiny88初體驗(三):串列埠

2023-08-22 18:02:29

ATtiny88初體驗(三):串列埠

ATtiny88微控制器不包含串列埠模組,因此只能使用軟體方式模擬串列埠時序。

串列埠通訊時序通常由起始位、資料位、校驗位和停止位四個部分組成,常見的設定為1位起始位、8位元資料位、無校驗位和1位停止位。

模擬串列埠傳送時序

  1. 設定TX引腳為輸出模式,初始電平狀態為高電平。
  2. 設定定時器週期,以9600波特率為例,將定時器週期設為 \(\frac{1s}{9600} \approx 104us\)
  3. TX引腳輸出低電平(起始位),同時開啟定時器。
  4. 之後的8次定時器中斷,每次輸出1位資料,從低位開始。
  5. 第9次定時器中斷,TX引腳輸出高電平(停止位)。
  6. 第10次定時器中斷,關閉定時器。

模擬串列埠接收時序

  1. 設定RX引腳為輸入模式,使能上拉電阻,開啟下降沿中斷。
  2. 當接收到起始位時,觸發下降沿中斷,設定定時器週期為 \(\frac{1s}{9600} \times \frac{1}{6} \approx 17us\) ,開啟定時器。
  3. 之後的30次定時器中斷,對RX引腳的電平狀態進行計數(起始位)。
  4. 第1次定時器中斷,將定時器週期重設為 \(\frac{1s}{9600} \times \frac{1}{3} \approx 35us\)
  5. 第3次定時器中斷,如果高電平數量大於低電平數量,則表示起始位接收失敗,直接關閉定時器,並開啟下降沿中斷。
  6. 第6/9/.../24/27次定時器中斷,判斷高電平和低電平的數量,選取數量多的那個電平作為資料位,從低位開始填充。
  7. 第30次定時器中斷,關閉定時器中斷,開啟下降沿中斷,如果高電平數量大於低電平數量,則表示成功接收到停止位,資料有效。

外部中斷

ATtiny88有8個外部中斷源:INT0、INT1、PCI0、PCI1、PCI2、PCI3。其中INT0/1支援低電平/下降沿/上升沿觸發,PCI0/1/2/3在引腳狀態改變時觸發。

ATtiny88外部中斷和引腳的對應關係如下:

中斷源 引腳
INT0 PD2
INT1 PD3
PCI0 PB[0:7] -> PCINT[0:7]
PCI1 PC[0:7] -> PCINT[8:15]
PCI2 PD[0:7] -> PCINT[16:23]
PCI3 PA[0:3] -> PCINT[24:27]

注意:即使引腳設定為輸出模式,也能觸發相應的中斷。

暫存器

  • ISC1[1:0] :設定INT1中斷觸發方式。
  • ISC0[1:0] :設定INT0中斷觸發方式,取值同 ISC1[1:0]

  • INT1 :設為1使能INT1中斷。
  • INT0 :設為1使能INT0中斷。

  • INTF1 :INT1中斷標誌位,執行中斷函數時自動清零,也可以寫1清零。
  • INTF0 :INT0中斷標誌位,執行中斷函數時自動清零,也可以寫1清零。

  • PCIE3 :設為1使能PCI3(PCINT[27:24])中斷。
  • PCIE2 :設為1使能PCI2(PCINT[23:16])中斷。
  • PCIE1 :設為1使能PCI1(PCINT[15:8])中斷。
  • PCIE0 :設為1使能PCI0(PCINT[7:0])中斷。

  • PCIF3 :PCI3(PCINT[27:24])中斷標誌位,執行中斷函數時自動清零,也可以寫1清零。
  • PCIF2 :PCI2(PCINT[23:16])中斷標誌位,執行中斷函數時自動清零,也可以寫1清零。
  • PCIF1 :PCI1(PCINT[15:8])中斷標誌位,執行中斷函數時自動清零,也可以寫1清零。
  • PCIF0 :PCI0(PCINT[7:0])中斷標誌位,執行中斷函數時自動清零,也可以寫1清零。

  • PCINTx :設為1使能PCINTx中斷。

程式碼實現

程式碼檔案的整體結構如下:

.
├── Makefile
├── inc
│   └── serial.h
└── src
    ├── main.c
    └── serial.c

inc/serial.h 標頭檔案的程式碼內容如下:

#pragma once

#include <stdint.h>

#define UART    (&serial)

typedef struct {
    const uint8_t *cfg;
    uint8_t flag;
    uint8_t tx_idx;
    uint8_t tx_temp;
    uint8_t tx_data;
    uint8_t rx_idx;
    uint8_t rx_temp;
    uint8_t rx_data;
    uint8_t rx_cnt;
} serial_t;

typedef enum {
    SERIAL_BR_1200 = 0,
    SERIAL_BR_2400,
    SERIAL_BR_4800,
    SERIAL_BR_9600,
    SERIAL_BR_19200,
    SERIAL_BR_38400,
    SERIAL_BR_57600,
    SERIAL_BR_115200
} serial_baudrate_t;

typedef enum {
    SERIAL_FLAG_TXE = 0x01,
    SERIAL_FLAG_RXNE = 0x02
} serial_flag_t;

extern serial_t serial;

void serial_setup(serial_t *serial, serial_baudrate_t br);
uint8_t serial_get_flag(serial_t *serial, serial_flag_t flag);
void serial_send_data(serial_t *serial, uint8_t data);
uint8_t serial_receive_data(serial_t *serial);

src/serial.c 原始檔的程式碼內容如下,其中將PD1引腳定義為TX,將PD2引腳定義為RX:

#include <serial.h>
#include <avr/io.h>
#include <avr/interrupt.h>

serial_t serial;

static const uint8_t serial_cfg[] = {
    0x03, 208, 35, 69,  // 1200
    0x03, 104, 17, 35,  // 2400
    0x03, 52, 9, 17,    // 4800
    0x02, 208, 35, 69,  // 9600
    0x02, 104, 17, 35,  // 19200
    0x02, 52, 9, 17,    // 38400
    0x02, 35, 6, 12,    // 57600
    0x01, 139, 23, 46,  // 115200
};

void serial_setup(serial_t *serial, serial_baudrate_t br)
{
    serial->cfg = &serial_cfg[br * 4];
    serial->flag = SERIAL_FLAG_TXE;         // initial value for serial->flag

    // setup tx pin
    PORTD |= _BV(PORTD1);                   // PD1 outputs high level
    DDRD |= _BV(DDD1);                      // set PD1 as output

    // setup rx pin
    PORTD |= _BV(PORTD2);                   // enable PD2 pull-up resistance
    DDRD &= ~_BV(DDD2);                     // set PD2 as input

    // setup INT0
    EICRA &= ~(_BV(ISC01) | _BV(ISC00));
    EICRA |= _BV(ISC01);                    // the falling edge of INT0 generates an interrupt request
    EIFR = _BV(INTF0);                      // clear INT0 interrupt flag
    EIMSK |= _BV(INT0);                     // enable INT0 interrupt

    // setup TIMER0
    TCNT0 = 0;                              // clear counter
    TIMSK0 = 0;                             // disable all interrupts of TIMER0
    TIFR0 = _BV(OCF0B) | _BV(OCF0A);        // clear TIMER0_COMPA & TIMER0_COMPB interrupt flags
    TCCR0A = serial->cfg[0];                // set mode & prescaler of TIMER0
}

uint8_t serial_get_flag(serial_t *serial, serial_flag_t flag)
{
    return serial->flag & flag;
}

void serial_send_data(serial_t *serial, uint8_t data)
{
    serial->flag &= ~SERIAL_FLAG_TXE;       // clear TXE flag
    serial->tx_data = data;                 // store the data to transmit
    serial->tx_temp = data;
    serial->tx_idx = 0;                     // reset index of transmission

    OCR0A = TCNT0 + serial->cfg[1] - 1;     // set period of TIMER0_COMPA
    PORTD &= ~_BV(PORTD1);                  // PD1 outputs low level
    TIFR0 = _BV(OCF0A);                     // clear TIMER0_COMPA interrupt flag
    TIMSK0 |= _BV(OCIE0A);                  // enable TIMER0_COMPA interrupt
}

uint8_t serial_receive_data(serial_t *serial)
{
    uint8_t data = serial->rx_data;         // read the data received
    serial->flag &= ~SERIAL_FLAG_RXNE;      // clear RXNE flag
    return data;
}

static inline void serial_tx_timer_isr(serial_t *serial)
{
    if (serial->tx_idx < 8) {               // send databits
        if (serial->tx_temp & 0x01) {       // output the lowest bit
            PORTD |= _BV(PORTD1);
        } else {
            PORTD &= ~_BV(PORTD1);
        }
        serial->tx_temp >>= 1;
    } else if (serial->tx_idx == 8) {       // send stopbit
        PORTD |= _BV(PORTD1);
    } else {                                // end of transmission
        serial->flag |= SERIAL_FLAG_TXE;    // set TXE flag
        TIMSK0 &= ~_BV(OCIE0A);             // disable TIMER0_COMPA interrupt
    }

    OCR0A += serial->cfg[1];                // set time of the next interrupt
    serial->tx_idx++;                       // update index of transmission
}

static inline void serial_rx_int_isr(serial_t *serial)
{
    OCR0B = TCNT0 + serial->cfg[2] - 1;     // set time of the first TIMER0_COMPB interrupt
    EIMSK &= ~_BV(INT0);                    // disable INT0 interrupt
    TIFR0 = _BV(OCF0B);                     // clear TIMER0_COMPB interrupt flag
    TIMSK0 |= _BV(OCIE0B);                  // enable TIMER0_COMPB interrupt
    serial->rx_idx = 0;                     // reset index of reception
    serial->rx_cnt = 0;                     // clear counter of 0/1
}

static inline void serial_rx_timer_isr(serial_t *serial)
{
    serial->rx_cnt += PIND & _BV(PIND2) ? 0x10 : 0x01;  // count 0/1

    if (serial->rx_idx == 2) {              // receive startbit
        if (serial->rx_cnt > 0x20) {        // if startbit is '1'
            TIMSK0 &= ~_BV(OCIE0B);         // disable TIMER0_COMPB interrupt
            EIFR = _BV(INTF0);              // clear INT0 interrupt flag
            EIMSK |= _BV(INT0);             // enable INT0 interrupt flag
        }
        serial->rx_cnt = 0;                 // reset counter of 0/1
    } else if (serial->rx_idx == 29) {      // receive stopbit
        if (serial->rx_cnt > 0x20) {        // if stopbit is '1'
            serial->rx_data = serial->rx_temp;  // the data received is valid, store it to serial->rx_data
            serial->flag |= SERIAL_FLAG_RXNE;   // set RXNE flag
        }
        TIMSK0 &= ~_BV(OCIE0B);             // disable TIMER0_COMPB interrupt
        EIFR = _BV(INTF0);                  // clear INT0 interrupt flag
        EIMSK |= _BV(INT0);                 // clear INT0 interrupt flag
    } else if (serial->rx_idx % 3 == 2) {   // receive databits
        serial->rx_temp >>= 1;
        if (serial->rx_cnt > 0x20) {
            serial->rx_temp |= 0x80;
        }
        serial->rx_cnt = 0;                 // reset counter of 0/1
    }

    OCR0B += serial->cfg[3];                // set time of the next interrupt
    serial->rx_idx++;                       // update index of reception
}

ISR(TIMER0_COMPA_vect)
{
    uint8_t sreg = SREG;
    serial_tx_timer_isr(UART);
    SREG = sreg;
}

ISR(INT0_vect)
{
    uint8_t sreg = SREG;
    serial_rx_int_isr(UART);
    SREG = sreg;
}

ISR(TIMER0_COMPB_vect)
{
    uint8_t sreg = SREG;
    serial_rx_timer_isr(UART);
    SREG = sreg;
}

注意:實測115200以下(含)的波特率傳送都正常,但是9600以上(不含)的波特率接收不正常,建議日常使用9600波特率。

重定向stdio到串列埠

為了更方便的使用串列埠,可以將標準輸入輸出重定向到串列埠,在AVR GCC中的做法如下:

  1. 定義輸入和輸出的介面函數,原型如下:
    int putc(char c, FILE *stream);
    int getc(FILE *stream);
    
  2. 使用 FDEV_SETUP_STREAM 建立一個stream。
    FILE s = FDEV_SETUP_STREAM(putc, getc, flag)
    
  3. 將上面建立的stream替換掉 stdout / stdin
    stdout = stdin = &s;
    

程式碼實現

src/main.c 原始檔的程式碼內容如下:

#include <stdint.h>
#include <stdio.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <serial.h>

static void stdio_setup(void);

int main(void)
{
    cli();
    stdio_setup();
    sei();

    printf("Hello, ATtiny88!\r\n");
    for (;;) {
        putchar(getchar());
    }
}

static int serial_putchar(char c, FILE *stream)
{
    while (!serial_get_flag(UART, SERIAL_FLAG_TXE));
    serial_send_data(UART, c);
    return 0;
}

static int serial_getchar(FILE *stream)
{
    while (!serial_get_flag(UART, SERIAL_FLAG_RXNE));
    return serial_receive_data(UART);
}

static void stdio_setup(void)
{
    static FILE f = FDEV_SETUP_STREAM(serial_putchar, serial_getchar, _FDEV_SETUP_RW);
    serial_setup(UART, SERIAL_BR_9600);
    stdout = &f;
    stdin = &f;
}

參考資料

  1. avr-libc: <stdio.h>: Standard IO facilities
  2. ATtiny88 Datasheet