В последнее время разработчики электроники испытывают трудности с поставками электронных компонентов. Одним из решений данной проблемы является переход на "исконно китайскую" элементную базу. Это решение подкупает ценой и доступностью, но пугает плохой документацией и небольшим количеством информации на понятных нам языках. В данной публикации расскажу о любопытном микроконтроллере на ядре RISC-V и сделаю простое первое устройство - датчик концентрации углекислого газа в воздухе/мигалку OLED дисплеем и светодиодом (куда ж без мигалки светодиодом). В репозитории размещен проект для тех, кто захочет воспользоваться данными наработками.

Вступление

Достаточно часто я слышал про ядро RISC-V и его нарастающую популярность. Мне захотелось попробовать микроконтроллер на этом ядре. Микрон недавно выпустил достаточно мощный МК на ядре RISC-V, но его закупку я даже не рассматривал, так как частному лицу достаточно сложно получить отладочный комплект для российского микроконтроллера.

Так же мне попадались МК CH32 от китайской фирмы WCH. Она широко известна преобразователем USB-UART CH340. Я посмотрел документацию и решил заказать младший МК из семейства RISC-V. Меня ждал приятный сюрприз: на али за 1400 рублей предлагают две демонстрационные платы, программатор и по пять МК одним лотом!

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

Периферия МК CH32V003
Периферия МК CH32V003

Мигаем светодиодом

Мигание светодиодом - канон первого действия при программировании МК. В аппаратной части для этого достаточно демонстрационной платы, программатора и нескольких проводов.

Первым шагом станет установка среды разработки. Производитель рекомендует 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)

Рассмотрим аргументы функции:

  1. I2C_TypeDef *i2c_periph - указатель на структуру, содержащую сведения об интерфейсе. Так как I2C в данном МК один, то возможный вариант только один: I2C1;

  2. uint8_t DevAddress - адрес ведомого устройства;

  3. uint8_t *pData - указатель на массив байт, который требуется передать;

  4. uint16_t Size - количество байт, которые требуется передать;

  5. 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".
При отключении датчика переменная sensor_disconnect становится равной "1".

Заключение

Работать с МК CH32V003 мне понравилось. Хоть он не обладает библиотеками и конфигуратором, как STM32, но все же несложный и понятный. Данная статья не является инструкцией по изготовлению "Душнометра", а показывает мой способ работы с интерфейсом I2C данного микроконтроллера.

Если вам понравилась данная публикация, то подписывайтесь на мой телегра... Oh, SHI~. У меня же его нет и всем надоела реклама этих ваших каналов!

Надеюсь, что принесу некоторую пользу своей интерпретацией I2C для CH32V003. В репозитории есть заготовка работы с SPI и описанная выше I2C. Буду шаг за шагом приоткрывать завесу китайской тайны.

Комментарии (17)


  1. Javian
    09.12.2023 19:41

    Интересно бы увидеть тесты SCD41 в сравнении с, например, датчиком K30.


    1. Kudriavyi Автор
      09.12.2023 19:41

      На scd41 заявляют +-40 ppm до 5000 ppm. У меня других датчиков для сравнения нет, да и немного это даст. Посмотрю, может найду ПГС (поверочная газовая смесь) на углекислоту или смеси какие-нибудь, где лаборатория анализы сделала, тогда сделаю обзор с показаниями. Но надо учитывать, что датчик этот с али и он может показывать цену на огурцы


  1. Std137
    09.12.2023 19:41

    О сколько нам открытий чудных, готовит просвещенья день...

    Блин, вы как с луны свалились. Россия уже на FPGA китайские перешла, а тут все контроллеры, помигать диодами.

    Я понимаю на старте фотку кота любимого на монитор или телек по hdmi, это круто. А потом, ядро risk v самому зашить, а сверху еще и код написать к нему.

    За 1000руб, нехилый набор. И флэшка, и кард ридер, lvds(40 pin), rgba, hdmi, и кучу портов. А да, в нагрузку еще две кнопки и шесть светодиодов :)

    Давайте уже расти...

    Пора расти.


    1. Kudriavyi Автор
      09.12.2023 19:41

      Не надо путать мягкое с круглым. Микроконтроллеры и ПЛИС - это немножко разные вещи. А так я полностью "за" новую информацию и с удовольствием почитаю про китайские ПЛИС.


    1. acc0unt
      09.12.2023 19:41

      Вот возьми и напиши статью про китайские FPGA - где брать девборды и софт, как с этим работать, и как в них заталкивать процессорные ядра и интерфейсы.

      А то вкатиться в комменты и гнуть пальцы - дело нехитрое. Я так тоже могу. Только толку от этого чуть.


  1. LAutour
    09.12.2023 19:41

    Разве SDK нужно качать отдельно? Вроде в MounRiver Studio все уже есть..


    1. Kudriavyi Автор
      09.12.2023 19:41

      Действительно, есть библиотеки и шаблоны для разных МК. Но примеров работы с периферией я не нашел в MounRiver Studio, а с примерами сильно попроще. Да и производитель на странице МК пишет "качай SDK по ссылке".


  1. HOMPAIN
    09.12.2023 19:41

    Особенность архитектуры RISC-V как-то проявляет себя в коде или в работе контроллера?


    1. Kudriavyi Автор
      09.12.2023 19:41

      Я не заметил никакой разницы. Программирование похоже на другой китайский МК (gd32), потому что они оба похожи названиями регистров на stm32.

      Возможно особенности кроются в работе прерываний и самого ядра, но это не точно. А интерфейс I2C китайцы могли дружно суть с stm32, как и остальную периферию. Я задумываю сделать краткий обзор на микросхемы АЦП от китайцев, там есть на что посмотреть.

      Главную особенность архитектуры RISC-V, о которой пишут в интернете, я заметил - ch32v003 можно даже в чип и дипе за 20 рублей купить, но в корпусе qfn-20


      1. HOMPAIN
        09.12.2023 19:41

        Действительно, цена интересная и напряжение 3.3 и 5в поддерживается. Может оказаться хорошей заменой atmega


    1. jaha33
      09.12.2023 19:41

      Если не требуется сложная математика с dsp функциями, то разницы между чистым risc-v и cortex m0 по сути нет. Разве что под разные компиляторы надо будет всякие тонкости переделывать


      1. Polarisru
        09.12.2023 19:41

        А где именно Cortex-M0 может серьезно помочь со сложной математикой и DSP? FP у него нет, встроенного деления тоже нет.


  1. halfworld
    09.12.2023 19:41

    Что-то со схемой не то. Светодиод катодом к "+", резистор 10 кОм...


    1. Kudriavyi Автор
      09.12.2023 19:41

      Есть такая буква в этом слове, ошибся со светодиодом, исправлю. А 10 кОм для современного индикаторного светодиода нормальный номинал. Если большой ток в светодиод дать, то он будет ярко мигать и раздражать. Два знакомых программиста их заклеивают даже????


      1. strvv
        09.12.2023 19:41

        да и общий ток на м/с надо выдерживать в необходимых размерах ;)
        а так - сам при прочтении стал искать в комментариях вопрос про светодиод.


  1. Megobari
    09.12.2023 19:41

    А всегда ли Risc-V быстрее?


    1. Kudriavyi Автор
      09.12.2023 19:41

      Как в комментариях выше отписали, что в сравнении с cortex m0 вряд ли большие отличия. Но ch32v003 по корпусу являет собой замену stm8s003, и stm8 со своим восьмибитным ядром вряд ли приблизится к ch32. И не все измеряется скоростью, пример тому ардуино с АВР, на не быстрый микроконтроллер сделали ещё абстракций и многих устраивает. Это может не так сильно влиять на устройство при промышленном производстве (микроволновка интеграл по поверхности не считает, например), а вот цена (и ещё доступность, в последнее время) - серьёзный фактор.