Програмний 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 кадри на секунду.

Коментарі
Дописати коментар