Програмний I2C проти апаратного: запускаємо U8x8 на CH32V003

У проєкті з OLED-дисплеєм та мікроконтролером STM32F103 виниклопитання: чи можна це здешевити? Що, якщо використати CH32V003? Багато ніжок не потрібно, корпусу SOP16 вистачить. Головне питання — як він працюватиме з OLED SSD1306.

Перевіримо це.

Як я писав раніше, U8x8 має легко портуватися і може працювати з програмним I2C (bit-bang). Перенесення коду з CMSIS пройшло дуже швидко — кілька чашок кави, і приклад у мене запрацював. Ось код функції u8x8_gpio_and_delay_ch32v:

uint8_t u8x8_gpio_and_delay_ch32v(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
    switch(msg) {
        case U8X8_MSG_GPIO_AND_DELAY_INIT:
            RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
            GPIO_InitTypeDef init = {0};
            init.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
            init.GPIO_Mode = GPIO_Mode_Out_OD;
            init.GPIO_Speed = GPIO_Speed_50MHz;
            GPIO_Init(GPIOA, &init);
            break;
        case U8X8_MSG_DELAY_MILLI:
            Delay_Ms(arg_int);
            break;
        case U8X8_MSG_DELAY_10MICRO:
            for (volatile uint32_t i = 0; i < (arg_int * 8); i++) { __NOP(); }
            break;
        case U8X8_MSG_DELAY_100NANO:
            __NOP();
            break;

        case U8X8_MSG_GPIO_I2C_CLOCK:
            GPIO_WriteBit(GPIOA, GPIO_Pin_2, (BitAction)(arg_int ? Bit_SET : Bit_RESET));
            break;

        case U8X8_MSG_GPIO_I2C_DATA:
            GPIO_WriteBit(GPIOA, GPIO_Pin_1, (BitAction)(arg_int ? Bit_SET : Bit_RESET));
            break;
    }
    return 1;
}

Головний нюанс під час налаштування пінів — використовувати режим відкритого стоку (GPIO_Mode_Out_OD). Для самого OLED-дисплея це, можливо, і не критично, але деякі I2C-пристрої також можуть притягувати шину до землі. Якщо пін працюватиме у звичайному Push-Pull режимі, це може призвести до короткого замикання VDD на GND.

Після цих налаштувань програмний I2C чудово завівся:


Проте зупинятися на цьому не хотілося. Мені стало цікаво, тому я продовжив експерименти і підключив бібліотеку U8x8 ще й за допомогою апаратного (Hardware) I2C. Обидва методи я свідомо залишив у коді, щоб наочно порівняти, наскільки апаратна реалізація виявиться швидшою за “ногодриг”.

Більше того, я протестував роботу дисплея не лише на стандартній частоті I2C 400 кГц, а й розігнав шину до 1600 кГц — це виявилася максимальна частота, на якій контролер дисплея ще стабільно приймає дані. Хм, можливо, вдалося б вичавити й більше, якби я вдався до овервольтингу :)

Отже, пропоную подивитися на результати нашого порівняння:

У цифрах маємо таку картину:

  • Програмний I2C (bit-bang) — видав близько 3 кадрів на секунду (332 мс на малювання одного кадру). Цілком прийнятна швидкість, якщо виводити лише статичну інформацію.

  • HW I2C на 400 кГц — показав 12 кадрів на секунду (83 мс на кадр). З таким FPS вже можна робити повноцінний UI, малювати меню та активні елементи.

  • HW I2C на 1600 кГц — видав цілих 32 кадри на секунду (31,6 мс на кадр). З такою швидкістю вже можна крутити плавні анімації, показувати просте відео або навіть зробити емулятор якоїсь ретро-консолі (щоправда, в монохромному вигляді).

Гаразд, досить теорії. Ось готовий C-код для тестування:

#include <ch32v00x.h>
#include <debug.h>
#include <stdlib.h>

#include "clib/u8x8.h"

#define USE_HW_I2C
#define I2C_FREQ 400000  // 400 kHz

void NMI_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void HardFault_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

uint8_t u8x8_gpio_and_delay_ch32v(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
    switch(msg) {
        case U8X8_MSG_GPIO_AND_DELAY_INIT:
            RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
            GPIO_InitTypeDef init = {0};
            init.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
            init.GPIO_Mode = GPIO_Mode_Out_OD;
            init.GPIO_Speed = GPIO_Speed_50MHz;
            GPIO_Init(GPIOC, &init);
            break;
        case U8X8_MSG_DELAY_MILLI:
            Delay_Ms(arg_int);
            break;
        case U8X8_MSG_DELAY_10MICRO:
            for (volatile uint32_t i = 0; i < (arg_int * 8); i++) { __NOP(); }
            break;
        case U8X8_MSG_DELAY_100NANO:
            __NOP();
            break;

        case U8X8_MSG_GPIO_I2C_CLOCK:
            GPIO_WriteBit(GPIOC, GPIO_Pin_2, (BitAction)(arg_int ? Bit_SET : Bit_RESET));
            break;

        case U8X8_MSG_GPIO_I2C_DATA:
            GPIO_WriteBit(GPIOC, GPIO_Pin_1, (BitAction)(arg_int ? Bit_SET : Bit_RESET));
            break;
    }
    return 1;
}

uint8_t u8x8_byte_hw_i2c_ch32v(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
    uint8_t *data;
    switch(msg) {
        case U8X8_MSG_BYTE_INIT:
            RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE);
            RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

            // We will use PC1 (SDA) and PC2 (SCL)
            GPIO_InitTypeDef GPIO_InitStructure = {0};
            GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
            GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // Alternate Function Open-Drain
            GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
            GPIO_Init(GPIOC, &GPIO_InitStructure);

            I2C_InitTypeDef I2C_InitTSturcture = {0};
            I2C_InitTSturcture.I2C_ClockSpeed = I2C_FREQ;
            I2C_InitTSturcture.I2C_Mode = I2C_Mode_I2C;
            I2C_InitTSturcture.I2C_DutyCycle = I2C_DutyCycle_2;
            I2C_InitTSturcture.I2C_OwnAddress1 = 0x00;
            I2C_InitTSturcture.I2C_Ack = I2C_Ack_Enable;
            I2C_InitTSturcture.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
            
            I2C_Init(I2C1, &I2C_InitTSturcture);
            I2C_Cmd(I2C1, ENABLE);
            break;

        case U8X8_MSG_BYTE_START_TRANSFER:
            while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) != RESET);

            I2C_GenerateSTART(I2C1, ENABLE);
            while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // Чекаємо EV5

            I2C_Send7bitAddress(I2C1, u8x8_GetI2CAddress(u8x8), I2C_Direction_Transmitter);
            while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // Чекаємо EV6
            break;

        case U8X8_MSG_BYTE_SEND:
            data = (uint8_t *)arg_ptr;
            while (arg_int > 0) {
                I2C_SendData(I2C1, *data++);
                while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
                arg_int--;
            }
            break;

        case U8X8_MSG_BYTE_END_TRANSFER:
            I2C_GenerateSTOP(I2C1, ENABLE);
            break;
        default:
            break;
    }
    return 1;
}

void refreshTest(u8x8_t *u8x8) {
    const int8_t columnsCount = 16;
    char buffer[columnsCount + 1];
    uint8_t counter = 0;
    uint32_t start_ticks = SysTick->CNT;
    SysTick->CTLR |= 1; // Enable SysTick
    
    while (1) {
        for (int8_t i = 0; i < sizeof(buffer) - 1; i++) {
            buffer[i] = '0' + counter;
        }
        counter++;
        if (counter > 30) {
            uint32_t end = SysTick->CNT;
            uint32_t elapsed = end - start_ticks;
            printf("Elapsed ticks\t%lu\n", elapsed);
            start_ticks = SysTick->CNT;
            counter = 0;
        }
        buffer[columnsCount] = '\0';
        for (int8_t y = 0; y < 8; y++) {
            u8x8_DrawString(u8x8, 0, y, buffer);
        }
    }
}

int main(void) {
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(9600);

    printf("Current SYSCLK: %lu\n", 
           SystemCoreClock);

    // u8x8 initialization block
    u8x8_t u8x8;
#ifdef USE_HW_I2C
    u8x8_Setup(&u8x8, 
        u8x8_d_ssd1306_128x64_noname, 
        u8x8_cad_ssd13xx_i2c, 
        u8x8_byte_hw_i2c_ch32v, 
        u8x8_gpio_and_delay_ch32v
    );
#else
    // Software i2c
    u8x8_Setup(
        &u8x8, 
        u8x8_d_ssd1306_128x64_noname, 
        u8x8_cad_ssd13xx_i2c, 
        u8x8_byte_sw_i2c, 
        u8x8_gpio_and_delay_ch32v
    );
#endif

    u8x8_InitDisplay(&u8x8);
    u8x8_SetPowerSave(&u8x8, 0);
    u8x8_ClearDisplay(&u8x8);
    u8x8_SetFont(&u8x8, u8x8_font_chroma48medium8_r);
    u8x8_DrawString(&u8x8, 0, 0, "Hello from CH32V");
    u8x8_DrawString(&u8x8, 0, 2, "SW I2C");
    Delay_Ms(3000);

    refreshTest(&u8x8);
    return 0;
}

void NMI_Handler(void) {}
void HardFault_Handler(void)
{
    while (1)
    {
    }
}

Щоб вам було зручно самостійно поекспериментувати, я виніс вибір режиму роботи та налаштування тактової частоти шини у невеличкий блок макросів на самому початку коду:

#define USE_HW_I2C
#define I2C_FREQ 400000  // 400 kHz

Керувати тестами дуже просто: якщо закоментувати рядок #define USE_HW_I2C, програма автоматично перемкнеться на використання програмного I2C. А за допомогою параметра I2C_FREQ ви можете легко регулювати швидкість апаратного інтерфейсу. Наприклад, змініть значення на 1600000, щоб повторити мій експеримент із розгоном і побачити 32 кадри на секунду.

Коментарі

Популярні дописи з цього блогу

Огляд DC-DC Step-down Buck перетворювачів

Як додати підтримку CH32V до Platform.IO

ESP8266 модуль з OLED екраном (HW-364A)