В последнее время разработчики электроники испытывают трудности с поставками электронных компонентов. Одним из решений данной проблемы является переход на "исконно китайскую" элементную базу. Это решение подкупает ценой и доступностью, но пугает плохой документацией и небольшим количеством информации на понятных нам языках. В данной публикации расскажу о любопытном микроконтроллере на ядре 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" и показания обновятся по готовности.
Заключение
Работать с МК CH32V003 мне понравилось. Хоть он не обладает библиотеками и конфигуратором, как STM32, но все же несложный и понятный. Данная статья не является инструкцией по изготовлению "Душнометра", а показывает мой способ работы с интерфейсом I2C данного микроконтроллера.
Если вам понравилась данная публикация, то подписывайтесь на мой телегра... Oh, SHI~. У меня же его нет и всем надоела реклама этих ваших каналов!
Надеюсь, что принесу некоторую пользу своей интерпретацией I2C для CH32V003. В репозитории есть заготовка работы с SPI и описанная выше I2C. Буду шаг за шагом приоткрывать завесу китайской тайны.
Комментарии (17)
Std137
09.12.2023 19:41О сколько нам открытий чудных, готовит просвещенья день...
Блин, вы как с луны свалились. Россия уже на FPGA китайские перешла, а тут все контроллеры, помигать диодами.
Я понимаю на старте фотку кота любимого на монитор или телек по hdmi, это круто. А потом, ядро risk v самому зашить, а сверху еще и код написать к нему.
За 1000руб, нехилый набор. И флэшка, и кард ридер, lvds(40 pin), rgba, hdmi, и кучу портов. А да, в нагрузку еще две кнопки и шесть светодиодов :)
Давайте уже расти...
Пора расти.
Kudriavyi Автор
09.12.2023 19:41Не надо путать мягкое с круглым. Микроконтроллеры и ПЛИС - это немножко разные вещи. А так я полностью "за" новую информацию и с удовольствием почитаю про китайские ПЛИС.
acc0unt
09.12.2023 19:41Вот возьми и напиши статью про китайские FPGA - где брать девборды и софт, как с этим работать, и как в них заталкивать процессорные ядра и интерфейсы.
А то вкатиться в комменты и гнуть пальцы - дело нехитрое. Я так тоже могу. Только толку от этого чуть.
LAutour
09.12.2023 19:41Разве SDK нужно качать отдельно? Вроде в MounRiver Studio все уже есть..
Kudriavyi Автор
09.12.2023 19:41Действительно, есть библиотеки и шаблоны для разных МК. Но примеров работы с периферией я не нашел в MounRiver Studio, а с примерами сильно попроще. Да и производитель на странице МК пишет "качай SDK по ссылке".
HOMPAIN
09.12.2023 19:41Особенность архитектуры RISC-V как-то проявляет себя в коде или в работе контроллера?
Kudriavyi Автор
09.12.2023 19:41Я не заметил никакой разницы. Программирование похоже на другой китайский МК (gd32), потому что они оба похожи названиями регистров на stm32.
Возможно особенности кроются в работе прерываний и самого ядра, но это не точно. А интерфейс I2C китайцы могли дружно суть с stm32, как и остальную периферию. Я задумываю сделать краткий обзор на микросхемы АЦП от китайцев, там есть на что посмотреть.
Главную особенность архитектуры RISC-V, о которой пишут в интернете, я заметил - ch32v003 можно даже в чип и дипе за 20 рублей купить, но в корпусе qfn-20
HOMPAIN
09.12.2023 19:41Действительно, цена интересная и напряжение 3.3 и 5в поддерживается. Может оказаться хорошей заменой atmega
jaha33
09.12.2023 19:41Если не требуется сложная математика с dsp функциями, то разницы между чистым risc-v и cortex m0 по сути нет. Разве что под разные компиляторы надо будет всякие тонкости переделывать
Polarisru
09.12.2023 19:41А где именно Cortex-M0 может серьезно помочь со сложной математикой и DSP? FP у него нет, встроенного деления тоже нет.
halfworld
09.12.2023 19:41Что-то со схемой не то. Светодиод катодом к "+", резистор 10 кОм...
Kudriavyi Автор
09.12.2023 19:41Есть такая буква в этом слове, ошибся со светодиодом, исправлю. А 10 кОм для современного индикаторного светодиода нормальный номинал. Если большой ток в светодиод дать, то он будет ярко мигать и раздражать. Два знакомых программиста их заклеивают даже????
strvv
09.12.2023 19:41да и общий ток на м/с надо выдерживать в необходимых размерах ;)
а так - сам при прочтении стал искать в комментариях вопрос про светодиод.
Megobari
09.12.2023 19:41А всегда ли Risc-V быстрее?
Kudriavyi Автор
09.12.2023 19:41Как в комментариях выше отписали, что в сравнении с cortex m0 вряд ли большие отличия. Но ch32v003 по корпусу являет собой замену stm8s003, и stm8 со своим восьмибитным ядром вряд ли приблизится к ch32. И не все измеряется скоростью, пример тому ардуино с АВР, на не быстрый микроконтроллер сделали ещё абстракций и многих устраивает. Это может не так сильно влиять на устройство при промышленном производстве (микроволновка интеграл по поверхности не считает, например), а вот цена (и ещё доступность, в последнее время) - серьёзный фактор.
Javian
Интересно бы увидеть тесты SCD41 в сравнении с, например, датчиком K30.
Kudriavyi Автор
На scd41 заявляют +-40 ppm до 5000 ppm. У меня других датчиков для сравнения нет, да и немного это даст. Посмотрю, может найду ПГС (поверочная газовая смесь) на углекислоту или смеси какие-нибудь, где лаборатория анализы сделала, тогда сделаю обзор с показаниями. Но надо учитывать, что датчик этот с али и он может показывать цену на огурцы