Здравствуй, Хабр! Случилось так, что совпали три события. Коллега подарил красивый винтажный миллиамперметр, по почте пришла платка с CH592F на борту и возникло желание продолжать изучать BLE (Bluetooth Low Energy). А изучение всегда интереснее совместно с решением какой-либо прикладной задачи. В статье хочу поделиться опытом создания индикатора на основе микроконтроллера CH592. Рассмотрим что такое TMOS, настроим инструменты для создания прошивки. Напишем программу для микроконтроллера. Она будет управлять стрелкой миллиамперметра через характеристику профиля устройства. Создадим «верхнее» программное обеспечение для компьютера. Оно будет принимать загрузку процессора от операционной системы компьютера и отправлять это значение на индикатор.
Почему именно CH592?
Есть у меня часы, называются Fenix 7. Как инженер и человек занимающийся бегом я от них в восторге. Долго держат зарядку, очень удобный функционал (явно бегуны участвовали в создании) и эргономика. И захотелось мне сделать свои часы. Да не простые, а чтобы механическая стрелка время показывала. Идея потихоньку зреет, я изучаю BLE и микроконтроллеры в целом, делаю разные поделки с этой технологией. Есть небольшой опыт с Nordic Semiconductor и ESP32. Так как предполагается разработка и ручное изготовление платы в небольшом корпусе, для меня критично количество обвязки микроконтроллера. С «нордиками» и их старым SDK у меня получилось на основе найденного брелка от машины сделать маячок для ключей. Потом я взялся за Zephyr и тут застрял. Прошивка компилируется и прошивается, но ничего не работает. C ESP32 (особенно с С3) вроде всё получается, и токи приемлемы… Но, листая hackaday, наткнулся на проект маяка для сети apple find my с CH592. Стало интересно, минимум деталей, широкий диапазон питающих напряжений, наличие USB, встроенный LDO и полноценный PMU (power management unit). Ко всему сказанному нужно отметить вопрос цены и доступности средств разработки.

Подумал «Будем посмотреть» и заказал платку от WeAct studio на известном всем сайте по цене менее чем одна шаурма.
Настройка средств разработки
Переводится контроллер в режим прошивки зажатием кнопки «Boot» и подключением к USB. Никаких программаторов не надо. Для разработки существует собственная среда MounRiver или набор открытых инструментов RISC-V Embedded GCC12 (в итоге, MounRiver тоже работает с этими инструментами). Скачал, установил «Лунную Реку Моун» и как-то не пошло. Встроенные прошивальщики не видели контроллер без программатора, внешний вид не тот… Не стал терять время и сразу же перешел в MS VS CODE. Отправной точкой тут послужил этот репозиторий (спасибо Nikhil Robinson, он адаптировал стандартные примеры WCH под сборку средствами make, там же ссылка на программу прошивки — wchisp). В качестве наиболее подходящего примера мною был выбран BLE_GATTS
Для сборки проекта нужно добавить в переменную PATH операционной системы (если вы, как и я, работаете в windows) пути к GCC и whisp. Moun River загружает инструменты при первом запуске (примерно по такому пути …\MounRiver_Studio2\resources\app\resources\win32\components\WCH\Toolchain\RISC-V Embedded GCC12\bin). Открываем проект в VS CODE и находим там MakeFile.

В MakeFile, в самом верху изменяем префикс цепочки инструментов…
# TOOLCHAIN_PREFIX := riscv-none-embed TOOLCHAIN_PREFIX := riscv-none-elf
… и пробуем собрать прошивку командой make из каталога приложения (app). Make так же должен быть виден через переменную PATH и запускаться из терминала bash. В случае успеха должен появиться файл peripheral.hex . Если у вас в терминале на этом этапе ошибки, нужно проверить наличие и доступность инструментов. У меня собралось сразу.
П р и м е ч а н и е. При первом включении платы в USB разъем в режиме прошивки и попытки загрузки программы может появится сообщение о некорректном USB драйвере. Он переключается при помощи утилиты Zadig и должен быть WinUSB. Как это было у меня — пока устройство в режиме загрузки (секунд 30), нужно быстро найти его в выпадающем списке Zadig и нажать кнопку ReplaceDriver. Делается один раз.

Далее в командной строке набираем wchisp flash ./peripheral.hex (ввод пока не нажимаем), зажимаем кнопку boot на плате, вставляем USB разъем в плату, отпускаем boot и вот теперь нажимаем ввод. Должен начаться процесс записи, либо будет сообщение о некорректном драйвере (смотри примечание выше). После прошивки, при помощи телефона и приложения LightBlue (nRF connect и подобных BLE эксплореров) сканируем эфир. Если в найденных устройствах присутствует «Simple peripheral» — поздравляю, средства разработки настроены!

Анализ кода примера
Перед тем как начать разбираться с исходниками рекомендую ознакомиться со статьей Владимира Печерских. Там доступно расписана база понятий спецификации BLE.
Целью статьи не является объяснение работы протокола (в виду обширности и сложности предмета на это статьи точно не хватит). Мы хотим получить устройство с определенным функционалом, однако, приведу цитату из статьи Владимира, она мне помогла с пониманием структуры профиля устройства:
Представьте себе BLE устройство в качестве книжного шкафа с несколькими полками. Каждая полка — это отдельная тематика. К примеру, у нас есть полки с фантастикой, математикой, энциклопедиями. На каждой полке стоят книги с указанной темой. А в некоторых книгах есть даже бумажные закладки с записями. Кроме того, у нас есть небольшой бумажный каталог всех книг. Если помните школьные библиотеки — это узкий ящик с бумажными карточками. При такой аналогии шкаф — это профиль нашего устройства. Полки — это сервисы, книги — характеристики, а каталог — это таблица атрибутов. Закладки в книгах — это дескрипторы ….
Приложение у нас пока состоит из двух файлов peripheral_main.c и peripheral.с . Первый файл представлен ниже. Из него для меня было новым упоминания о какой-то TMOS. К ней мы ещё вернемся.
/********************************** (C) COPYRIGHT ******************************* * File Name : main.c * Author : WCH * Version : V1.1 * Date : 2020/08/06 * Description : 澶栬浠庢満搴旂敤涓诲嚱鏁 Говорят по китайски.... #include "CONFIG.h" // Конфигурация микроконтроллера #include "HAL.h" // Абстракция от железа #include "gattprofile.h" // Объявления, касаемые Generic Attribute Profile #include "peripheral.h" // Включения из второга файла проекта /*GLOBAL TYPEDEFS*/ __attribute__((aligned(4))) uint32_t MEM_BUF[BLE_MEMHEAP_SIZE / 4]; //атрибуты __attribute__((aligned(4))) uint32_t g_cmd[64]; //для упаковки структур #if(defined(BLE_MAC)) && (BLE_MAC == TRUE) const uint8_t MacAddr[6] = {0x84, 0xC2, 0xE4, 0x03, 0x02, 0x02}; #endif /********************Main_Circulation*************************/ __HIGH_CODE __attribute__((noinline)) void Main_Circulation() // Какая-то главная циркуляция, пока не понятно. { while(1) { /* mDelaymS(1000); // Тут я мигал встроенным светодиодом GPIOA_SetBits(GPIO_Pin_8); mDelaymS(1000); GPIOA_ResetBits(GPIO_Pin_8); mDelaymS(1000); GPIOA_SetBits(GPIO_Pin_8); mDelaymS(1000); GPIOA_ResetBits(GPIO_Pin_8); mDelaymS(1000);* TMOS_SystemProcess(); // Какой-то менеджер, пока не понятно } } /***************main*********************/ int main(void) // Это понятно, все начинается тут { //#if(defined(DCDC_ENABLE)) && (DCDC_ENABLE == TRUE) PWR_DCDCCfg(ENABLE); // Включение внутреннего преобразователя питания //#endif SetSysClock(CLK_SOURCE_PLL_60MHz); //Источник тактирующего сигнала #if(defined(HAL_SLEEP)) && (HAL_SLEEP == TRUE) GPIOA_ModeCfg(GPIO_Pin_All, GPIO_ModeIN_PU); // Это используется в режиме сна, GPIOB_ModeCfg(GPIO_Pin_All, GPIO_ModeIN_PU); // чтобы минимизировать утечки #endif #ifdef DEBUG // Тут настраивается светодиод, который работает индикатором при GPIOA_SetBits(bTXD1); // выбранной опции DEBUG GPIOA_ModeCfg(bTXD1, GPIO_ModeOut_PP_5mA); UART1_DefInit(); #endif PRINT("%s\r\n", VER_LIB); // Вывод логов через UART //GPIOA_ModeCfg(GPIO_Pin_8, GPIO_ModeOut_PP_20mA); //Так задается состояние режима //работы порта на выход CH59x_BLEInit(); //Инициализация BLE HAL_Init(); //Инициализация HAL GAPRole_PeripheralInit(); // Иницализация типа BLE устройства Peripheral_Init(); // Тут уже конкретная настройка профиля из peripheral.с Main_Circulation(); // Это запускает функцию выше, вроде похоже на RTOS, //но пока не понятно } /******************************** endfile @ main ******************************/
В файле peripheral.c уже гораздо больше кода. Тут настраивается GAP (Generic Access Profile) профиль. Он регламентирует то, как устройства будут соединяться. GATT (Generic Attribute Profile) профиль отвечает за то, как устройства будут обмениваться данными. Конкретно создается сервис ffe0 и четыре характеристики в нем.

Будем работать с первой характеристикой, её свойства позволяют писать и читать данные. Среди всего peripheral.c наибольший интерес в контексте задачи представляет самая нижняя функция — static void simpleProfileChangeCB(uint8_t paramID, uint8_t *pValue, uint16_t len). Эта функция вызывается когда происходит изменение значения первой или третьей характеристики. Самое простое, что тут можно сделать практически — мигнуть встроенным светодиодом, если произошел вызов этой функции.
static void simpleProfileChangeCB(uint8_t paramID, uint8_t *pValue, uint16_t len) { switch(paramID) { case SIMPLEPROFILE_CHAR1: // если изменилась характеристика 1, { // то мы попадаем в эту ветку tmos_memcpy(newValue, pValue, len); PRINT("profile ChangeCB CHAR1.. \r\n"); GPIOA_InverseBits( GPIO_Pin_8 ); DelayMs( 500 ); GPIOA_InverseBits( GPIO_Pin_8 ); DelayMs( 500 ); break; } case SIMPLEPROFILE_CHAR3: { uint8_t newValue[SIMPLEPROFILE_CHAR3_LEN]; tmos_memcpy(newValue, pValue, len); PRINT("profile ChangeCB CHAR3..\r\n"); break; } default: // should not reach here! break; } }
Этот измененный код был загружен в контроллер, и при отправке произвольного байта в характеристику 1 ожидаемо мигал светодиод. Однако, при установке задержки больше 600 мс и отправки произвольного байта соединение разрывалось. Тут мы подошли вплотную к TMOS.
Ставим задачи в TMOS
TMOS (Task Management Operating System) — это легковесная операционная система реального времени, предназначенная для управления задачами в embedded-системах (встраиваемых системах). Она предоставляет базовые механизмы для многозадачности, управления событиями, таймерами и сообщениями, что упрощает разработку сложных приложений на микроконтроллерах. (Так мне сказал DeepSeek, ещё я встречал, что TMOS это Time Management Operating System). Алгоритм работы с системой такой:
-
Зарегистрировать задачу:
-
Каждая задача регистрируется в TMOS с помощью функции TMOS_ProcessEventRegister.
-
-
Написать обработчики событий:
-
Каждая задача имеет функцию обработки событий, которая вызывается, когда задача получает события.
-
-
Запускать события разово или периодически:
-
TMOS позволяет запускать таймеры с заданным интервалом. Когда таймер истекает, генерируется событие.
-
-
Задачи могут обмениваться сообщениями, для этого зарезервировано событие SYS_EVENT_MSG.
-
Осуществить планирование:
-
При объявлении события ему назначается приоритет
-
Напишем задачу мигания светодиодом. Для этого создадим файлы led_task.h и led_task.c . Ниже заголовочный файл.
#ifndef LED_TASK_H #define LED_TASK_H #include "CONFIG.h" // Заголовочный файл для CH592 // Прототипы функций void LED_Task_Init(void); // Инициализация задачи, будет вызываться в peripheral_main // Сообщения задаче, будет вызываться в функции-коллбеке изменения характеристики, // в файле perephiral.c uint8_t Send_LED_Message( uint8_t *data , uint16_t length ); //Обработчик задачи. Тут будут обрабатываться события и сообщения uint16_t LED_Task_Handler(uint8_t taskID, uint16_t event); #endif // LED_TASK_
В led_task.c объявим переменные событий.
#define E1000MS_EVENT 0x0080 #define E100MS_EVENT 0x0040
Всего в задаче их может быть 15 и одно событие зарезервировано для сообщений. Шестнадцатибитное значение справа, это флаг события, который соответствует уникальному событию в одной и той же задаче. Флаг, равный 1, указывает на то, что событие, соответствующее этому биту, запущено, а значение 0 указывает на то, что оно не запущено. Далее следует непонятный момент c этим значением, ответ на который меня привел на сайт csdn (попадая на этот сайт, складывается ощущение, что попадаешь в аниме). Вот, скорее всего, не точный перевод про это значение:
Приоритет определяется на основе идентификатора задачи. Чем ниже приоритет, тем чаще задача будет выполняться первой.
Идентификатор задачи мы присвоим ниже и выбирать его мы не можем. Как я понял, чем меньше это число, тем выше приоритет у события. Пока не понятно, знатоки призываются в комментарии. Далее резервируем идентификатор задачи и присваиваем ей номер. В этом же месте запускаем задачу. Пока будем пробовать зажигать светодиод по команде.
tmosTaskID LED_TaskID = INVALID_TASK_ID; // Объявление идентификатора, пока без номера // Инициализация задачи светодиода void LED_Task_Init(void) { // Создание задачи в TMOS, её ригистрация, после этого TMOS_SystemProcess()сможет // с ней работать LED_TaskID = TMOS_ProcessEventRegister(LED_Task_Handler); // Запуск задачи (без интервала, так как управление будет через сообщения) tmos_start_task(LED_TaskID,SYS_EVENT_MSG); PRINT(" >>>>>> LED_Task initialized \n\r"); }
Команда будет отправляться из функции обратного вызова при изменении характеристики. Ниже код обработчика событий и функции отправки сообщений.
// Обработчик задачи светодиода uint16_t LED_Task_Handler( uint8_t task_id, uint16_t events ) { uint8_t *pMsg; // Указатель на сообщение if ( events & SYS_EVENT_MSG ) // Если произошло событие "Сообщение" { if ( (pMsg = tmos_msg_receive( task_id )) != NULL ) // если сообщение { // адресованно именно нашей задаче if(*pMsg == 34) { // Если в сообщении 0x24 PRINT(" >>>>>>>>>>>> in condition tmp - %d\n\r",tmp); // Мы точно попали tmos_start_task(LED_TaskID,E1000MS_EVENT,1600); // в нужное условие? } // Если да, то запустить событие, которое происходит раз в секунду tmos_msg_deallocate( pMsg ); } // return unprocessed events return (events ^ SYS_EVENT_MSG); // Возврат необработанных сообщений } if(events & E1000MS_EVENT) {// Мигание раз в секунду PRINT(" >>>>>> E1000MS_EVENT reached \n\r"); GPIOA_ModeCfg(GPIO_Pin_8, GPIO_ModeOut_PP_20mA); // Порт можно настроить 1 раз GPIOA_InverseBits(GPIO_Pin_8); tmos_start_task(LED_TaskID, E1000MS_EVENT, 1600);// Перезапуск таймера события //GPIOA_SetBits(GPIO_Pin_8); return(events ^ E1000MS_EVENT); } } // Discard unknown events return 0; } // Функция для отправки сообщения в задачу светодиода uint8_t Send_LED_Message( uint8_t *data , uint16_t length ) { uint8_t *p_data; if ( LED_TaskID != TASK_NO_TASK ) // Если задача действительно существует { // Send the address to the task p_data = tmos_msg_allocate(length); // Резервируем память под данные? if ( p_data ) { tmos_memcpy(p_data, data, length); // Копируем в выделенную память tmos_msg_send( LED_TaskID, p_data ); //значение и отправляем задаче PRINT("Data sended \n\r"); // Тут я разбирался с вопросом "указатель" и "куда он уазывает" PRINT("Send_LED_Message value >>> %x \r\n",*p_data); PRINT("Send_LED_Message pointer >>> %x \r\n",p_data); return ( SUCCESS ); //发送成功 Говорят по китайски } } return ( FAILURE ); //发送失败 Говорят по китайски }
В этом обработчике мной было потрачено много времени, пока я не разобрался. Оказывается, в TMOS работа идет кадрами по 625 мкс. Число 1600 в функции запуска задачи — это её таймер. 0.625мс*1600 = 1000 мс = 1с Если событие не выполняется за 625 мкс, то DeepSeek советует:
В TMOS задачи выполняются до тех пор, пока они не завершат свою работу или не передадут управление планировщику.
Время выполнения задачи зависит от её сложности и частоты вызова TMOS_SystemProcess.
Чтобы избежать блокировки других задач, разделяйте длительные операции на этапы и используйте события с задержкой.
Используя в начале Delay() я как — то блокировал другие задачи, в том числе и те, которые обслуживали BLE. Результат — разрыв соединения. Хотя задержки больше 625 мкс работали. Третий пункт из цитаты выше мне помог.
Еще сильно помог в понимании механизма работы событий вот такой псевдокод:

Проверим на практике. Для компиляции в makefile добавим свеженаписанные файлы, в секцию APP_C_SRCS += \
APP_C_SRCS += \ ./app/peripheral.c \ ./app/peripheral_main.c \ ./app/led_task.c
В switch-case колбека добавим функцию отправки сообщения, не забываем включить в начале peripheral.c #include "led_task.h"
... case SIMPLEPROFILE_CHAR1: { uint8_t test_data[] = {0x22, 0x59, 0xd8}; uint8_t newValue[SIMPLEPROFILE_CHAR1_LEN]; // В эту переменную пишем // значение из характеристики tmos_memcpy(newValue, pValue, len) PRINT("--------------profile ChangeCB CHAR1..------------------- \r\n"); PRINT("CHAR1 newValue>>> %x \r\n",*newValue); PRINT("CHAR1 pValue >>> %x \r\n",*pValue); Send_LED_Message(newValue,1); break; } ...
Тут несколько слов о отладке. Можно сделать так, чтобы CH592 поднимала com порт для вывода сообщений, и у людей даже получалось. Но у меня не получилось. Пробовал примеры USB_LOGI, USB_CDC. Ком порт создается, соединение происходит, но ничего не приходит. Возможно сообщество подскажет. Отладка производилась подключением к UART микроконтроллера внешнего преобразователя.
Окончательный код приложения
После того, как светодиод стал моргать с желаемым мной периодом, разработка пошла гладко. В файле gattpofile.h увеличил размер данных, передаваемых через характеристику.
// Length of characteristic in bytes ( Default MTU is 23 ) #define SIMPLEPROFILE_CHAR1_LEN 8 //1 was #define SIMPLEPROFILE_CHAR2_LEN 1 #define SIMPLEPROFILE_CHAR3_LEN 1 #define SIMPLEPROFILE_CHAR4_LEN 1 #define SIMPLEPROFILE_CHAR5_LEN 5
Импровизированный текстовый протокол передачи данных выглядит так — команда (первый байт) и значение (2 и 3 байты). При изменении характеристики в задачу отправляется сообщение из трех байт. Если первый байт соответствует команде работы со стрелкой, то запускается событие установки стрелки ШИМом по значению из двух следующих байт. Работа с ШИМ была взята из официального примера.
... if(*pMsg == 36) { // Команда на работу со стрелкой PRINT(" >>>>>>>>>>>> in condition %x \n\r",*pMsg); tmp = ((pMsg[1] << 8) + (pMsg[2])); //Собираем число из двух байт tmos_start_task(LED_TaskID,E100MS_EVENT,160); // Событие каждые 100 мс } ... if(events & E100MS_EVENT) { PRINT(" >>>>>> E100MS_EVENT reached \n\r"); GPIOA_ModeCfg(GPIO_Pin_12, GPIO_ModeOut_PP_5mA); // PA12 - PWM4 PWMX_CLKCfg(4); // cycle = 4/Fsys PWMX_16bit_CycleCfg(60000); // pwm_value=tmp PWMX_16bit_ACTOUT(CH_PWM4, pwm_value, High_Level, ENABLE);// Установка стрелки PRINT(" > in condition, command 24, pwm_value - %d\n\r",pwm_value); PRINT(" > array first element - %x,second element - %x \n\r",pMsg[0],pMsg[1]); tmos_start_task(LED_TaskID, E100MS_EVENT, 160); return(events ^ E100MS_EVENT); }
Опытным путем было выяснено, что вся шкала миллиамперметра это 34500 попугаев. Весь проект доступен в репозитории.
Верхнее (управляющее) ПО
Достаем лучезарный Python и при помощи …. собственных или коллективных знаний пишем программку —
import asyncio # bleak асинхронная библиотека, поэтому нужна asyncio import psutil # Получение данных о системе from bleak import BleakScanner, BleakClient # Работа с BLE # UUID 1 характеристики MY_CHAR_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb" async def scan_and_connect(): # Сканируем кефир devices = await BleakScanner.discover() for d in devices: print(d) # Получаем необходимые данные для соединения по имени device = await BleakScanner.find_device_by_name("Simple Peripheral") if not device: print("Device not found") return None client = BleakClient(device) # Соединяемся await client.connect() return client async def main(): # В начале был MAIN... client = await scan_and_connect() if not client: return try: while True: # Получаем загрузку процессора в процентах и ранжируем к попугаям cpu_percentage=int(psutil.cpu_percent()*345) # Формируем посылку print(cpu_percentage) byte_array = cpu_percentage.to_bytes(2, byteorder='big') # данные # Создаем новый массив байт с добавлением команды '\x24' в начало new_byte_array = bytes([0x24]) + byte_array #данные с командой print(new_byte_array) # Отправляем посылку await client.write_gatt_char(MY_CHAR_UUID, new_byte_array) # Ожидание 1 секунды await asyncio.sleep(0.2) finally: await client.disconnect() asyncio.run(main())
Корпус
С появлением 3D принтеров многие поделки любителей обрели законченный вид! Корпус моего изделия навеян устремлениями промышленного дизайна пятидесятых в аэродинамические авиационные формы. Ну или похож на нос картошкой. Как посмотреть.


Результаты
Интересная и сложная тема BLE для меня ещё немного приоткрылась. Удалось создать удаленный индикатор для показаний, например, температуры. Используя изученные механизмы передачи команд и параметров можно управлять какими-либо исполнительными механизмами.
В режиме ожидания ток потребления не более 4 мА, в режиме соединения не более 6 мА. На момент публикации прибор проработал не выключаясь 5 дней. Я поставил две подсевшие батарейки суммарным напряжением 2,6 В, ожидаю работу до 1.8 В. Приблизительная оценка — около 400 часов. Получился эдакий дожиратель батареек (battery eater). Это без оптимизаций с режимами сна. В качестве улучшения, можно сделать работу по такой схеме: компьютер распространяет рекламные пакеты (advertising) c интересующим меня параметром, а индикатор без соединения ждет такой пакет и извлекает информацию из него.

Прилагаю видео работы устройства
Заключение
В этом проекте широко использовал нейросеть. Задавал вопросы, на которые не мог найти ответы сам, или ответы были мне непонятны. Приятно удивлен! Работает и помогает!
Испытываете тягу к технологиям, но вас отталкивают сложности? Начинайте с простого! Поморгайте светодиодом, напишите программу, которая меняет цвет кнопки при её нажатии. Разберите непонятную функцию не в вашем коде, который используете (может даже сделайте её лучше). Желаю успехов в творчестве!
Скрытый текст
Нравятся мои поделки? Специально для вас веду канал о своих хобби → https://t.me/modelistconstruktor
ссылка на оригинал статьи https://habr.com/ru/articles/885148/
Добавить комментарий