SNSへはこちら

Longan NanoでRISC-Vチャレンジ(8) - シリアル通信

続いてシリアル通信です。このマイコンは

  • USART
  • SPI
  • I2C
  • I2S
  • CAN
  • USB

を使えるペリフェラルがありますが、前半3つの USARTSPII2C のみを対象に取り上げます。

特にハマりどころはありませんでしたので、さっぱりと述べて終わろうと思います。

USART

PC と通信する、マイコン間で通信するといったらこの通信規格です。ボーレートを正しく決めれば動いてしまうので楽ですよね。

コードはとても素直です。

void usart_init(void) {
    rcu_periph_clock_enable(RCU_USART0);
    rcu_periph_clock_enable(RCU_GPIOA);
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
    gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);

    USART_BAUD(USART0) = (58 << 4) | (10 << 0); // 115200bps on 108MHz
    USART_CTL0(USART0) = USART_CTL0_TEN | USART_CTL0_REN;
    USART_CTL0(USART0) |= USART_CTL0_UEN;
}

void usart_send(char ch) {
    while( !(USART_STAT(USART0) & USART_STAT_TBE) );
    USART_DATA(USART0) = ch;
    while( !(USART_STAT(USART0) & USART_STAT_TC) );
}

#include <stdarg.h>
void usart_printf(const char *fmt, ...) {
    char buf[100];
    va_list args;
    va_start(args, fmt);
    vsprintf(buf, fmt, args);
    va_end(args);

    char *p = buf;
    while( *p != '\0' ) {
        usart_send(*p);
        p++;
    }
}

usart_printf を使えば良く、使い方は printf と全く同様です。

ところで、このマイコンのライブラリに printf の内部実装が提供されているのはご存知でしょうか。Firmware/RISCV/stubs/write.c に実装がありますので見てみましょう。

typedef unsigned int size_t;

extern int _put_char(int ch) __attribute__((weak));

ssize_t _write(int fd, const void* ptr, size_t len) {
    const uint8_t * current = (const uint8_t *) ptr;

//  if (isatty(fd)) 
    {
        for (size_t jj = 0; jj < len; jj++) {
            _put_char(current[jj]);

            if (current[jj] == '\n') {
                _put_char('\r');
            }
        }
        return len;
    }

    return _stub(EBADF);
}

int puts(const char* string) {
    return _write(0, (const void *) string, strlen(string));
}

int _put_char(int ch)
{
    usart_data_transmit(USART0, (uint8_t) ch );
    while (usart_flag_get(USART0, USART_FLAG_TBE)== RESET){
    }

    return ch;
}

なんか勝手に _put_char が実装されていますが、幸いなことに weak 属性が付与されていますので自前で実装することができます。printf 自体かなりプログラムサイズを食うのであまり使いたくはありませんが、汎用PC と同じように使いたい!という人は実装してみてはいかがでしょうか(丸投げ)。

SPI

USART とは違ってボーレート設定がどうでもいい感じの簡単なシリアル通信規格です。このマイコンでは F303 のように 8bit 固定ではなく、F103 のように 16bit での利用もできます。本当に F303 と F103 のいいとこ取りをしたマイコンだなあ。

NSS(or CS#) は GPIO で駆動させることもできますが、今回はすべてペリフェラルに任せて動かすということをしました。「NSS パルスモード」とか用語が分かりにくいのでマニュアルの説明部分をよく読むことをおすすめします。

void spi0_init(void) {
    // PA4: NSS
    // PA5: SCK
    // PA6: MISO
    // PA7: MOSI
    rcu_periph_clock_enable(RCU_GPIOA);
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_7);
    gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_6);

    rcu_periph_clock_enable(RCU_SPI0);
    SPI_CTL0(SPI0) = (0b111 << 3) | SPI_CTL0_FF16 | SPI_CTL0_MSTMOD; // 421,875Hz, 16bit, MSB first, master mode
    SPI_CTL1(SPI0) |= SPI_CTL1_NSSP;
    SPI_CTL0(SPI0) |= SPI_CTL0_SPIEN;
}

void spi0_send(uint16_t dat) {
    while( !(SPI_STAT(SPI0) & SPI_STAT_TBE) );
    SPI_DATA(SPI0) = dat;
}

I2C

I2C はどのマイコンでも悩まされることが多いのですが、比較的簡単に実装できました。プルアップは外部抵抗でどうぞ。

#include <gd32vf103.h>

void i2c_init(void) {
    // PB6: SCL
    // PB7: SDA
    rcu_periph_clock_enable(RCU_GPIOB);
    gpio_init(GPIOB, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_6 | GPIO_PIN_7);

    rcu_periph_clock_enable(RCU_I2C0);
    I2C_CTL1(I2C0) |= 54; // 54MHz clock input
    I2C_CKCFG(I2C0) = 540; // divided into 100kHz
    I2C_CTL0(I2C0) |= I2C_CTL0_I2CEN;
}

void wait(int n) {
    for(volatile int i = 0; i < n; i++);
}

void i2c_send(uint8_t addr, uint8_t *dat, int len) {
    I2C_CTL0(I2C0) |= I2C_CTL0_START;
    while( !(I2C_STAT0(I2C0) & I2C_STAT0_SBSEND) );

    I2C_DATA(I2C0) = addr;
    while( !(I2C_STAT0(I2C0) & I2C_STAT0_ADDSEND) );
    const volatile uint16_t stat = I2C_STAT1(I2C0);
    (void)stat;

    for(int i=0; i<len; i++) {
        I2C_DATA(I2C0) = dat[i];
        while( !(I2C_STAT0(I2C0) & I2C_STAT0_TBE) );
    }
    while( !(I2C_STAT0(I2C0) & I2C_STAT0_BTC) );

    I2C_CTL0(I2C0) |= I2C_CTL0_STOP;
    while( I2C_STAT0(I2C0) & I2C_STAT0_BTC );
    while( I2C_CTL0(I2C0) & I2C_CTL0_STOP );
}

init 内 CTL1 で設定している値は、「クロックを何分周するか」ではなく「今のクロックは何MHzか」をそのまま打ち込むものなので誤解なきよう。