
В последнее время разработчики электроники испытывают трудности с поставками электронных компонентов. Одним из решений данной проблемы является переход на "исконно китайскую" элементную базу. Это решение подкупает ценой и доступностью, но пугает плохой документацией и небольшим количеством информации на понятных нам языках. В данной публикации расскажу о любопытном микроконтроллере на ядре RISC-V и сделаю простое первое устройство - датчик концентрации углекислого газа в воздухе/мигалку OLED дисплеем и светодиодом (куда ж без мигалки светодиодом). В репозитории размещен проект для тех, кто захочет воспользоваться данными наработками.
Вступление
Достаточно часто я слышал про ядро RISC-V и его нарастающую популярность. Мне захотелось попробовать микроконтроллер на этом ядре. Микрон недавно выпустил достаточно мощный МК на ядре RISC-V, но его закупку я даже не рассматривал, так как частному лицу достаточно сложно получить отладочный комплект для российского микроконтроллера.
Так же мне попадались МК CH32 от китайской фирмы WCH. Она широко известна преобразователем USB-UART CH340. Я посмотрел документацию и решил заказать младший МК из семейства RISC-V. Меня ждал приятный сюрприз: на али за 1400 рублей предлагают две демонстрационные платы, программатор и по пять МК одним лотом!

CH32V003 позиционируется как замена восьмибитного МК STM8S003F3, совместим с ним по ногам, но обладает большим быстродействием. Стоимость CH32V003 порядка 25 рублей за штуку. Так же немаловажно, что данный МК поставляется российскими поставщиками электронных компонентов.

Мигаем светодиодом
Мигание светодиодом - канон первого действия при программировании МК. В аппаратной части для этого достаточно демонстрационной платы, программатора и нескольких проводов.
Первым шагом станет установка среды разработки. Производитель рекомендует MounRiver Studio. Основа данной среды - Eclipse. Я бы порекомендовал выбирать расположение среды так, чтобы путь к папке и имя пользователя в операционной системе не содержали кириллицы (с данной средой не проверял, но уже имел неприятный опыт работы с другими средами разработки на базе Eclipse, которые отказывались работать)
Вторым шагом необходимо скачать SDK с сайта производителя МК. Внизу станицы есть ссылка на скачивание ZIP архива со схемой демонстрационной платы и примерами ПО. Распаковываем архив в рабочую папку.
Копируем проект GPIO_Toggle в отдельную папку с сохранением уровня вложенности папок. Я переименовал проект в Habr_CO2.

Отрываем проект в среде разработки. Пример GPIO_Toggle управляет светодиодом, подключенным к порту PD_0. В моем устройстве светодиод подключен к порту PD_3.

В инициализации и в бесконечном цикле мигания меняем номер вывода порта:
void GPIO_Toggle_INIT(void) { GPIO_InitTypeDef GPIO_InitStructure = {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//здесь был GPIO_Pin_0 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); } ... while(1) { Delay_Ms(500); GPIO_WriteBit(GPIOD, GPIO_Pin_3, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET)); } //и в строке выше был GPIO_Pin_0
Теперь можно скомпилировать проект. Нажимаем Project -> Build Project. После компиляции нажимаем Flash -> Download, дожидаемся окончания прошивки, судорожно нажимаем Reset или выключаем и включаем МК - ничего не работает! А все потому, что мы изменили имя проекта и имя файла hex тоже изменилось. А среда разработки прошивает старый файл hex. Нажимаем Flash -> Configuration, в поле Target File: выбираем Habr_CO2.hex. Теперь после прошивки светодиод будет мигать (у меня все работает).
Работаем с I2C
Как говорила преподавательница по матану в МАИ (первый факультет, кто был там, вспомнит): "Внимательный слушатель заме...етит", что в схеме нет подтягивающих резисторов шины I2C к "+" питания, но все работает, так как на платах датчика CO2 и дисплея данные резисторы есть. Начнем с OLED дисплея. Он сделан на контроллере ssd1306 и про него написаны гигабайты текста, множество библиотек, так что описывать подробно работу с дисплеем я не стану. Для работы с дисплеем нужен интерфейс I2C. Посмотрев примеры работы с I2C накатывает ностальгия по простым готовым решениям от фирмы ST. Но что делать - придется писать HAL и дорабатывать библиотеку работы с дисплеем.
На данный момент я сделал библиотеку работы с I2C в блокирующем режиме. Микроконтроллеры STM приучили меня, что блокирующие функции работы с интерфейсами (а по-хорошему и не с интерфейсами) должны быть защищены от зависания - например выполнять в теле функции проверку времени ее исполнения, и при превышении оного выходить из функции и возвращать ошибку TIMEOUT. Для отсчета времени используется SysTick таймер. Реализацию базовых функций системного таймера я вынес в отдельную библиотеку, которую необходимо подключить в проект.
Чтобы не таскать все библиотеки HAL в каждый проект, я положил их в папку на одном уровне с папкой проекта. Чтобы проект нормально собирался, надо указать компилятору путь к этой папке. Для этого открываем свойства проекта нажав Project -> Properties. В открывшемся окне в списке выбираем C/C++ General -> Path and symbols, в поле Languages переключаем на GNU C и в поле include directories добавляем строку "/${ProjName}/HAL". Эта строка позволит подключить внешнюю папку к проекту.

В дереве проекта щелкаем правой кнопкой мыши по имени проекта и добавляем папку командой Add -> External Linked Folder. Файлы внутри папки добавятся автоматически.
При создании библиотеки "i2c_hal.h" я опирался на пример I2C_EEPROM. Инициализацию интерфейса взял из примера. Подробно не буду разбирать функцию, так как стандартная инициализация покроет ~95% всех случаев работы с интерфейсом.
На картинке ниже вызывается ведомое устройство с адресом 0b1101000 для последующей передачи данных от ведущего к ведомому. На верхней схеме сигналов ведомое устройство ответило, что готово принимать данные, а на нижней ответа не поступило (например устройства с таким адресом на шине нет).

Передачу некоторого количества байт вынесем в функцию:
uint8_t HAL_I2C_Transmit (I2C_TypeDef *i2c_periph, uint8_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t i2c_timeout)
Рассмотрим аргументы функции:
I2C_TypeDef *i2c_periph- указатель на структуру, содержащую сведения об интерфейсе. Так как I2C в данном МК один, то возможный вариант только один:I2C1;uint8_t DevAddress- адрес ведомого устройства;uint8_t *pData- указатель на массив байт, который требуется передать;uint16_t Size- количество байт, которые требуется передать;uint32_t i2c_timeout- максимальное время пребывания в функции в отсчетах системного таймера (миллисекундах).
При входе в любую функцию передачи или приема данных создаем переменную и записываем в нее текущее значение системного таймера. Это значение будет использовано для вычисления Timeout-а.
uint32_t tickstart = get_tick();
Перед тем, как генерировать сигнал старт, необходимо убедиться, что шина не занята. Проверяем флаг I2C_FLAG_BUSY и ждем, когда он сбросится, но не дольше установленного времени пребывания в функции. После сброса флага генерируем старт и ждем установки флага I2C_EVENT_MASTER_MODE_SELECT. Это значит, что наше устройство захватило управление шиной.
/* i2c master sends start signal only when the bus is idle */ while((I2C_GetFlagStatus( i2c_periph, I2C_FLAG_BUSY ) != RESET) && (get_tick() - tickstart < i2c_timeout)) ; if(get_tick() - tickstart < i2c_timeout) { } else { return I2C_BUSY; } /* send the start signal */ I2C_GenerateSTART( i2c_periph, ENABLE ); while(!I2C_CheckEvent( I2C1, I2C_EVENT_MASTER_MODE_SELECT ) && (get_tick() - tickstart < i2c_timeout)) ; if(get_tick() - tickstart < i2c_timeout) { } else { return I2C_BUSY; }
Теперь можно отправить адрес ведомого устройства. Надо учитывать, что адрес должен быть сдвинут на один бит влево, так как младший бит будет обозначать направление передачи данных (с данным вопросом у меня часто возникает путаница, например в библиотеках МК фирмы Nordic не надо сдвигать адрес влево). Чтобы установить младший бит адреса в режим передачи данных в качестве второго аргумента функции I2C_Send7bitAddress укажем I2C_Direction_Transmitter. Прежде чем передавать данные, дождемся ответа от ведомого, если такой адрес есть на шине. Для этого будем проверять установку флага I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED. Если ведомого с указанным адресом не окажется на шине, от зависания нас спасет проверка времени нахождения в функции. Функция пошлет на шину I2C сигнал стоп и вернет значение I2C_ADDR_NACK.
I2C_Send7bitAddress( i2c_periph, DevAddress, I2C_Direction_Transmitter ); /* address flag set means i2c slave sends ACK */ while((!I2C_CheckEvent( i2c_periph, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED )) && (get_tick() - tickstart < i2c_timeout)); if(get_tick() - tickstart < i2c_timeout) { } else { I2C_GenerateSTOP( i2c_periph, ENABLE ); return I2C_ADDR_NACK; }
Передаем полезную информацию. В цикле передаем один байт функцией I2C_SendData( i2c_periph, *pData ) , инкрементируем указатель на буфер, чтобы на следующем проходе передать следующий байт и ждем, когда установится флаг окончания передачи I2C_EVENT_MASTER_BYTE_TRANSMITTED. После успешной передачи данных отправляем на шину сигнал СТОП и возвращаем I2C_OK.
for(count = 0; count < Size; count ++) { I2C_SendData( i2c_periph, *pData ); /* increment pointer to the next byte to be written */ pData++; /* wait until transmission complete */ while((!I2C_CheckEvent( i2c_periph, I2C_EVENT_MASTER_BYTE_TRANSMITTED )) && (get_tick() - tickstart < i2c_timeout)); if(get_tick() - tickstart < i2c_timeout) { } else { I2C_GenerateSTOP( i2c_periph, ENABLE ); return I2C_TIMEOUT; } } I2C_GenerateSTOP( i2c_periph, ENABLE ); return I2C_OK;
Теперь рассмотрим функцию приема массива байт:
uint8_t HAL_I2C_Receive (I2C_TypeDef *i2c_periph, uint8_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t i2c_timeout)
Аргументы функции приема такие же, как и у функции передачи, с разницей лишь в том, что передается указатель на массив, в который будут помещены данные и количество байт, которые необходимо принять.
Запрос ведомого устройства на прием данных отличается незначительно от передачи ему данных. При отправке адреса ведомого устройства на шину, необходимо установить младший бит адреса в "1". Изменим второй аргумент функции I2C_Send7bitAddress( i2c_periph, DevAddress, I2C_Direction_Receiver ). Затем дождемся установки флага I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED и переведем МК в режим ACK вызвав функцию I2C_AcknowledgeConfig( i2c_periph, ENABLE ).
/* i2c master sends start signal only when the bus is idle */ while((I2C_GetFlagStatus( i2c_periph, I2C_FLAG_BUSY ) != RESET) && (get_tick() - tickstart < i2c_timeout)) ; if(get_tick() - tickstart < i2c_timeout) { } else { return I2C_BUSY; } /* send the start signal */ I2C_GenerateSTART( I2C1, ENABLE ); while(!I2C_CheckEvent( I2C1, I2C_EVENT_MASTER_MODE_SELECT ) && (get_tick() - tickstart < i2c_timeout)) ; if(get_tick() - tickstart < i2c_timeout) { } else { I2C_GenerateSTOP( i2c_periph, ENABLE ); return I2C_BUSY; } I2C_Send7bitAddress( i2c_periph, DevAddress, I2C_Direction_Receiver ); /* address flag set means i2c slave sends ACK */ while((!I2C_CheckEvent( i2c_periph, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED )) && (get_tick() - tickstart < i2c_timeout)); if(get_tick() - tickstart < i2c_timeout){ } else { I2C_GenerateSTOP( i2c_periph, ENABLE ); return I2C_ADDR_NACK; } I2C_AcknowledgeConfig( i2c_periph, ENABLE );
Принимаем данные от ведомого. В цикле ждем установки флага I2C_FLAG_RXNE, то есть пока в буфере не окажется очередной байт данных. Затем помещаем принятый байт в массив *pData = I2C_ReceiveData( i2c_periph ) и инкрементируем указатель на этот массив pData++. После приема предпоследнего байта необходимо указать ведомому, что следующий байт последний. Когда счетчик count будет равен номеру предпоследнего байта, вызовем функцию I2C_AcknowledgeConfig( i2c_periph, DISABLE ). по окончанию приема данных проверяем, не превысило ли время TIMEOUT, если нет - то данные приняты успешно, можно послать сигнал СТОП.
for(count = 0; count < Size; count ++) { /* wait until data received */ while((I2C_GetFlagStatus( i2c_periph, I2C_FLAG_RXNE ) == RESET) && (get_tick() - tickstart < i2c_timeout)); if( count == (Size-2) ) { /*before receiving last byte and after receiving byte-1 disable ACK for last byte */ I2C_AcknowledgeConfig( i2c_periph, DISABLE ); } /* receive data and store in buffer */ *pData = I2C_ReceiveData( i2c_periph ); pData++; } /* check timeout while receiving data */ if(get_tick() - tickstart > i2c_timeout) { I2C_GenerateSTOP( i2c_periph, ENABLE ); return I2C_TIMEOUT; } I2C_GenerateSTOP( i2c_periph, ENABLE ); return I2C_OK;
Этого достаточно для работы с интерфейсом I2C (кого-то устраивают библиотеки на регистрах или программный I2C), но после общения с STM32 мне нравятся функции чтения и записи регистров/памяти ведомого устройства.
uint8_t HAL_I2C_Mem_Write (I2C_TypeDef *i2c_periph, uint8_t DevAddress, uint8_t MemAddress, uint8_t *pData, uint16_t Size, uint32_t i2c_timeout) uint8_t HAL_I2C_Mem_Read (I2C_TypeDef *i2c_periph, uint8_t DevAddress, uint8_t MemAddress, uint8_t *pData, uint16_t Size, uint32_t i2c_timeout)
Функция HAL_I2C_Mem_Write представляет собой ту же самую функцию передачи данных ведомому с добавлением перед массивом байт одного байта внутреннего адреса памяти/регистра uint8_t MemAddres.
Функция HAL_I2C_Mem_Read совмещает в себе передачу одного байта адреса памяти/регистра ведомому и приема массива данных от ведомого.
Время помигать дисплеем
В качестве основы я использую данную библиотеку работы с SSD1306 достаточно давно на разнообразных МК. Скопируем папку с библиотекой дисплея и шрифтом в папку User в корне проекта. Добавим в дерево проекта папку ssd1306 командой Add -> Existing Folder пропишем в меню Path & Symbols /${ProjName}/User/ssd1306 по аналогии с библиотекой HAL.
Изменения коснулись функций SSD1306_writecommand и SSD1306_UpdateScreen. В них были заменены функции передачи данных по I2C на вышеописанные функции для CH32V003. В файле "SSD1306.h" подключаем новые библиотеки #include "i2c_hal.h" и #include "systick_hal.h". В main.c подключаем #include "SSD1306.h" и #include "systick_hal.h" и приступаем к работе с дисплеем.
Перед бесконечным циклом инициализируем SysTick, I2C, ssd1306 и управляем дисплеем в бесконечном цикле. Так же меняем все задержки с Delay_Ms(500) на delay_1ms(500), так как эта функция сделана для работы вместе с системным таймером.
В функцию HAL_I2C_Init передаем частоту работы интерфейса в герцах и собственный адрес МК на шине I2C как ведомого устройства (в данном примере не используется, но функция инициализации производителем написана так).
В бесконечном цикле буфер дисплея заполняем черным цветом, формируем строку символов "Blink OFF". Начиная с верхнего левого угла на дисплей выводится строка символов и буфер выводится на дисплей. Затем, после задержки в полсекунды, дисплей заполняется белым и выводится новая строка.
uint8_t buff[32]; Systick_Init (); HAL_I2C_Init(400000, 0xEE); SSD1306_Init(); while(1) { SSD1306_Fill(SSD1306_COLOR_BLACK); sprintf (buff,"Blink OFF"); SSD1306_GotoXY(0, 0); SSD1306_Puts(buff, &Font_11x18, SSD1306_COLOR_WHITE); SSD1306_UpdateScreen(); delay_1ms(500); SSD1306_Fill(SSD1306_COLOR_WHITE); sprintf (buff,"Blink ON"); SSD1306_GotoXY(0, 0); SSD1306_Puts(buff, &Font_11x18, SSD1306_COLOR_BLACK); SSD1306_UpdateScreen(); delay_1ms(500); GPIO_WriteBit(GPIOD, GPIO_Pin_3, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET)); }
Тяжелая гиф мигания дисплеем

Измеряем концентрацию CO2
Для некоторых опытов с медицинским аппаратом я заказал модуль датчика CO2 SCD41.

Датчик работает по фотоакустическому принципу измерения. При воздействии ИК света на молекулы CO2 они начинают колебаться, что регистрируется микрофоном и обрабатывается встроенным МК (в общих чертах, как я понял). Данный датчик снабжен системой автоматической калибровки. Он регистрирует минимальное значение CO2 в окружающей среде, и принимает его за 400 ppm (концентрация углекислого газа в атмосфере у земли). Это значит, что помещение, в котором расположен датчик, хоть изредка, но проветривается до атмосферного уровня CO2. Из этого следует, что датчик в режиме автокалибровки не стоит помещать в атмосферу чистого газа. Я поместил его в атмосферу кислорода (по нормам содержание CO2 менее 100 ppm) и датчик стал сильно завышать показания. В своих опытах я отключаю автоматическую калибровку, но для бытового применения этого не требуется.
Для работы с датчиком существуют библиотека Arduino, но она написана на C++ и все равно не для CH32V003, так что я сделал свою реализацию. Копируем папку с библиотекой scd4x в проект и добавляем путь к ней, как это уже делали с библиотекой для ssd1306.
Чтобы улучшить переносимость библиотеки датчика, над функциями передачи и приема данных сделаем абстракции:
uint8_t scd4x_transmit_data (uint8_t *pData, uint16_t Size) { return HAL_I2C_Transmit(I2C_SCD4x, SCD4x_SLAVE_ADDR, pData, Size, I2C_Timeout); } uint8_t scd4x_receive_data (uint8_t *pData, uint16_t Size) { return HAL_I2C_Receive(I2C_SCD4x, SCD4x_SLAVE_ADDR, pData, Size, I2C_Timeout); }
Команды датчика состоят из двух байт (список команд и их значение можно посмотреть в описании на датчик). Отправку команд так же вынесем в отдельную функцию:
uint8_t scd4x_send_command (uint16_t command) { uint8_t buff[2]; buff [0] = (uint8_t)(command>>8); buff [1] = (uint8_t)command; return scd4x_transmit_data(buff, 2); }
В данном проекте ограничусь тремя функциями работы с датчиком. Первую необходимо будет вызвать для запуска периодических измерений.
uint8_t scd4x_Init(void) { uint8_t err_code; err_code = scd4x_send_command (SCD4x_START_PERIODIC_MEASUREMENT); return err_code; }
С инициализацией вряд ли возникнут сложности. Функция чтения данных от датчика уже несколько сложнее:
uint8_t scd4x_read_measurement(uint16_t* co2_conc, int16_t* temp, uint16_t* humidity) { uint8_t buff [10]; uint8_t err_code = 0; scd4x_send_command (SCD4x_READ_MEASUREMENT); err_code = scd4x_receive_data (buff, 9); if (err_code != 0) { return err_code; //return i2c err code in case of i2c troubles } if (buff[2] != sensirion_common_generate_crc(buff,2) ||buff[5] != sensirion_common_generate_crc(&buff[3],2) ||buff[8] != sensirion_common_generate_crc(&buff[6],2)) { return 0xFF;//crc error } *co2_conc = ((uint16_t)buff[0]<<8) + buff[1]; *temp = (int32_t)(((uint16_t)buff[3]<<8) + buff[4])*175/65535 - 45; *humidity = (int32_t)(((uint16_t)buff[3]<<8) + buff[4])*1000/65535; return 0; }
В качестве аргументов в данную функцию необходимо передать указатели на переменные, в которые будет помещено значение концентрации CO2 в ppm, температуры в градусах Цельсия и относительной влажности в процентах*10. В теле функции посылаем команду чтения значения SCD4x_READ_MEASUREMENT, принимаем 9 байт от датчика. Проверяем, не произошло ли каких-то проблем на шине I2C, и если они случились, то возвращаем код ошибки. Затем сравниваем контрольные суммы каждого значения с рассчитанной контрольной суммой. Функцию sensirion_common_generate_crc любезно предоставил производитель датчика. Если в одном из значений контрольная сумма не совпала - возвращаем crc_error. Затем приводим значения к реальным по формулам, указанным в описании, и возвращаем 0 как свидетельство того, что значения приняты успешно. В данном примере можно заметить, что при вычислении температуры сильно теряется точность, но датчик потребляет достаточно много энергии (средний ток 15 мА) и данный показатель показывает температуру датчика, но не окружающего воздуха. Данное измерение требуется для коррекции показаний внутри датчика.
В создании библиотеки работы с I2C мне очень помог логический анализатор. Я купил его не так давно и теперь не представляю своей жизни без него. Раньше интерфейсы отлаживал с использованием осцилографа, приходилось импульсы считать. А логический анализатор дает красивую и понятную картинку. В качестве примера рассмотрим функцию чтения scd4x_read_measurement в логическом анализаторе.

После сигнала СТАРТ (S) ведущий отправляет 0xC4 - это адрес ведомого 0x62<<1 с добавлением бита ЗАПИСЬ(W), получает от него ответ ACK(A), затем ведущий отправляет команду чтения данных от датчика 0xEC 0x05 (SCD4x_READ_MEASUREMENT). При передаче последнего байта ведомый ответил ACK, но ведущий уже закончил передачу и сгенерировал СТОП (P).

Для приема данных ведущий снова отправляет на шину СТАРТ (S), затем 0xC5 - это адрес ведомого 0x62<<1 с добавлением бита ЧТЕНИЕ(R), получает от ведомого ответ ACK(A) и начинает принимать данные. В конце каждого принятого байта ведущий отвечает ведомому ACK(A), и только при приеме последнего байта NACK(N), что видно на картинке ниже. Всего ведущий принимает 9 байт от ведомого, как и запланировано.

Так как период измерений составляет 5 секунд, а между измерениями датчик неохотно делится измеренными показаниями, нам необходимо проверять состояние готовности данных.
uint8_t scd4x_is_data_ready(void) { uint8_t buff [3]; uint16_t status = 0; uint8_t err_code = 0; scd4x_send_command (SCD4x_GET_DATA_READY_STATUS); err_code = scd4x_receive_data (buff, 3); if (err_code != 0) { return err_code; //return i2c err code in case of i2c troubles } status = ((uint16_t)buff[0]<<8) + buff[1]; status = status & 0x07FF; //set first 5 bits to zero if (status)return 0; //return 0 if data is ready else return SCD4x_DATA_NOT_READY; }
Функция похожа на чтение данных. Датчик возвращает значение uint16_t. Когда все младшие 11 бит равны нулю - данные измерения готовы для чтения. Поэтому положим принятые два байта в переменную status и обнулим старшие 5 бит. Если переменная окажется после этого равна 0 - данные готовы и функция вернет 0. Если же не готовы - возвращаем SCD4x_DATA_NOT_READY.
Добавим инициализацию датчика и вывод на дисплей названия устройства "Dushnometr" (написал бы на русском, но шрифт не поддерживает, а кириллицу мне добавлять очень лениво). Объявим переменные, в которых будут храниться данные и состояние датчика. Добавим переменные, хранящие время, для вычисления времени между измерениями значений и массив для формирования строк, выводимых на дисплей.
scd4x_Init(); SSD1306_Fill(SSD1306_COLOR_BLACK); sprintf (buff,"Dushnometr"); SSD1306_GotoXY(0, 0); SSD1306_Puts(buff, &Font_11x18, SSD1306_COLOR_WHITE); SSD1306_UpdateScreen(); delay_1ms(1500); uint16_t co2_conc = 0, rh = 0; int16_t temp = 0; uint32_t time = 0, prev_time = 0; uint8_t sensor_disconnect = 0; uint8_t err_code; uint8_t data[32];
Осталось в бесконечном цикле проверять готовность данных, и при готовности читать данные от датчика. Если произойдет разрыв связи с датчиком, после восстановления необходимо будет вызвать функцию инициализации, чтобы запустить измерение.
После выводим на дисплей показания концентрации углекислого газа, температуры, влажности и времени между измерениями. В каждом цикле добавим включение светодиода на 1 миллисекунду для индикации работы программы.
while(1) { err_code = scd4x_is_data_ready(); if (err_code == 0) //data ready { scd4x_read_measurement(&co2_conc, &temp, &rh); time = (get_tick() - prev_time)/100; prev_time = get_tick(); } else if (err_code != SCD4x_DATA_NOT_READY) //i2c bus error { sensor_disconnect = 1; } if (sensor_disconnect)//sensor reinit { if (scd4x_Init() == 0){ sensor_disconnect = 0; } } SSD1306_Fill(SSD1306_COLOR_BLACK); sprintf (data, "CO2:%dppm", co2_conc); SSD1306_GotoXY(0, 0); SSD1306_Puts(data, &Font_11x18, SSD1306_COLOR_WHITE); sprintf (data, "RH:%d.%d T:%d'C",rh/10, rh%10, temp); SSD1306_GotoXY(0, 25); SSD1306_Puts(data, &Font_7x10, SSD1306_COLOR_WHITE); sprintf (data, "time:%d.%d d/c:%d",time/10, time%10, sensor_disconnect); SSD1306_GotoXY(0, 40); SSD1306_Puts(data, &Font_7x10, SSD1306_COLOR_WHITE); SSD1306_UpdateScreen(); delay_1ms(10); GPIO_WriteBit(GPIOD, GPIO_Pin_3,Bit_RESET); delay_1ms(1); GPIO_WriteBit(GPIOD, GPIO_Pin_3,Bit_SET); }
Для отображения состояния сенсора выведем значение переменной sensor_disconnect на экран. Если вытащить датчик из разъема, то значение этой переменной станет "1", после подключения датчика значение станет снова "0" и показания обновятся по готовности.


sensor_disconnect становится равной "1".Заключение
Работать с МК CH32V003 мне понравилось. Хоть он не обладает библиотеками и конфигуратором, как STM32, но все же несложный и понятный. Данная статья не является инструкцией по изготовлению "Душнометра", а показывает мой способ работы с интерфейсом I2C данного микроконтроллера.
Если вам понравилась данная публикация, то подписывайтесь на мой телегра... Oh, SHI~. У меня же его нет и всем надоела реклама этих ваших каналов!
Надеюсь, что принесу некоторую пользу своей интерпретацией I2C для CH32V003. В репозитории есть заготовка работы с SPI и описанная выше I2C. Буду шаг за шагом приоткрывать завесу китайской тайны.
