Введение
Для одной небольшой задачи понадобилось подружить лазерный датчик расстояния VL53L1X с микроконтроллером CH32V003. Датчик работает по I2C, и изначально я рассчитывал, что в его даташите будут описаны регистры, чтобы быстро написать драйвер «с нуля». Но у STMicroelectronics подход другой: они не публикуют описание регистров, а распространяют готовый драйвер с API только для STM32 (см. UM2356 "VL53L1X API user manual").
Для моего проекта нужен был простой и дешёвый МК с минимальными ресурсами, и выбор пал на CH32V003F4P6. У него нет HAL и CubeMX, только CMSIS, «урезанный StdPeriph», reference manual и среда MounRiver. Впрочем, для простых проектов этого достаточно.
Выбор драйвера
Чтобы не тащить тяжёлый оригинальный драйвер, я взял облегчённый вариант — VL53L1X ultra lite driver (ULD), также выпущенный ST (описан в UM2510 "A guide to using the VL53L1X ultra lite driver"). На GitHub легко нашёл пример для STM32, скопировал к себе папку с исходниками VL53L1X_ULD_API и подключил в проект:

Дополнительно добавил свои файлы vl53l1x.c и vl53l1x.h, где сделал адаптацию под CH32.
Инициализация I2C
Весь порт свёлся к тому, чтобы переписать низкоуровневые функции передачи данных по I2C. Для начала я добавил инициализацию I2C в vl53l1x.c:
static void vl53l1x_LL_Init(void) { GPIO_InitTypeDef MyGPIO = {0}; I2C_InitTypeDef MyI2C = {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1 , ENABLE); MyGPIO.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2; // sda&scl MyGPIO.GPIO_Mode = GPIO_Mode_AF_OD; MyGPIO.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(GPIOC, &MyGPIO); MyGPIO.GPIO_Pin = GPIO_Pin_3 ; // xshut pin MyGPIO.GPIO_Mode = GPIO_Mode_Out_PP; MyGPIO.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(GPIOC, &MyGPIO); MyI2C.I2C_ClockSpeed = 100000; MyI2C.I2C_DutyCycle = I2C_DutyCycle_16_9; MyI2C.I2C_OwnAddress1 = 0; MyI2C.I2C_Mode = I2C_Mode_I2C; MyI2C.I2C_Ack = I2C_Ack_Enable; MyI2C.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, &MyI2C); I2C_Cmd(I2C1, ENABLE); I2C_AcknowledgeConfig(I2C1, ENABLE); }
Замена HAL-функций
В файле vl53l1_platform.c драйвер использует только две функции:
int I2CWrite(uint16_t Dev, uint8_t *pdata, uint32_t count)
int I2CRead(uint16_t Dev, uint8_t *pdata, uint32_t count)
В оригинале они опираются на HAL_I2C_Master_Receive и HAL_I2C_Master_Transmit. Я оставил названия теми же, но сделал собственную реализацию в файлах HAL_I2C.c и HAL_I2C.h.
Так драйвер остался полностью совместимым по интерфейсам. Плюс реализовал рабочие таймауты, чтобы избежать зависания при ошибках на шине I2C.
Системный таймер
Для работы таймаутов и функции HAL_Delay нужен системный таймер. В STM32 это делает CubeMX, но здесь пришлось поднять руками.
В system_ch32v00x.c я включил SysTick, настроил прерывание каждые 1 мс и добавил счётчик:
void SystemInit (void) { RCC->CTLR |= (uint32_t)0x00000001; RCC->CFGR0 &= (uint32_t)0xFCFF0000; RCC->CTLR &= (uint32_t)0xFEF6FFFF; RCC->CTLR &= (uint32_t)0xFFFBFFFF; RCC->CFGR0 &= (uint32_t)0xFFFEFFFF; RCC->INTR = 0x009F0000; RCC_AdjustHSICalibrationValue(0x10); SetSysClock(); NVIC_EnableIRQ(SysTicK_IRQn); SysTick->SR &= ~(1 << 0); SysTick->CMP = SystemCoreClock/1000; SysTick->CNT = 0; SysTick->CTLR = 0xF; } void SysTick_Handler(void) { SysCounter++; SysTick->SR = 0; } uint32_t GetSysTick(void) { return SysCounter; } void HAL_Delay(uint16_t ms) { uint32_t StartTick = GetSysTick(); while((GetSysTick()-StartTick)<ms); }
Пример работы
В итоговом примере для среды MounRiver датчик каждые 1 секунду отправляет актуальное расстояние через UART (пин PD5). Всё работает стабильно. API драйвера остаётся без изменений, поэтому его легко подключать к проекту.
Исходники проекта я выложил на GitHub: KorolDenis/Driver-VL53L1X-for-CH32V003
