基于 C 语言的线程安全的图像缓冲队列
线程安全的图像缓冲队列 1. 介绍 2. 关键代码 2.1 数据结构 2.2 入队出队核心函数 3. 内存管理注意事项 3.1 c_mat_t 和 mat_data 的生命周期 3.2 队列销毁时的内存清理： 4. 总结 线程安全的图像缓冲队列 在这篇博客中，笔者将探讨一个最近实现的基于 C 语言的线程安全图像缓冲循环队列，重点分析其内存管理的细节和注意事项。该队列用于在图像处理后入队，供后续 AI 推理时出队使用，特别适用于实时视频处理或 AI 推理流水线。以下笔者将聚焦内存管理的实现细节、潜在风险及优化建议。 1. 介绍 该图像缓冲队列是一个固定大小的线程安全循环队列，基于 OpenCV 的 Mat 结构存储图像数据，采用生产者-消费者模型。内存管理是笔者重点考虑的细节，需要确保队列在高负载场景下的稳定性和高效性： 生产者：预处理图像（如调整大小、归一化）并入队。 消费者：出队图像并送入 AI 模型进行推理。 队列的关键特性包括： 使用 POSIX 线程（ pthread ）的互斥锁和条件变量实现线程安全。 通过循环缓冲区优化内存使用。 支持图像元数据（名称、路径）和 OpenCV 图像数据的管理。 2. 关键代码 2.1 数据结构 c_mat_t：OpenCV Mat 的封装，存储图像数据及其元数据。 typedef struct c_mat { unsigned char* mat_data; int mat_data_size; int mat_width; int mat_height; int mat_channels; int mat_type; } c_mat_t; 内存管理要点：mat_data 是一个指针，指向动态分配的实际图像数据。调用者需负责分配和释放内存，队列本身不管理 mat_data 的生命周期。 ImageInfo 是存储图像元数据和 c_mat_t 指针; ImageQueue 是循环队列结构，包含 ImageInfo 数组和同步原语: typedef struct { char name[MAX_NAME_LEN]; char path[MAX_NAME_LEN]; c_mat_t *img; } ImageInfo; typedef struct { ImageInfo items[MAX_QUEUE_SIZE]; int front; int rear; int size; pthread_mutex_t mutex; pthread_cond_t not_full; pthread_cond_t not_empty; } ImageQueue; 内存管理要点：items 是一个固定大小的数组（MAX_QUEUE_SIZE = 10），在栈或全局内存中分配，无需动态分配。mutex 和条件变量由 POSIX 线程库管理。 2.2 入队出队核心函数 从该队列设计上讲：调用者手动管理每一张图片的内存创建与释放，因此初始化队列和销毁队列的设计比较简单且常规，不再赘述。 入队图像： int enqueue(ImageQueue *queue, const char *name, const char *path, const c_mat_t *img) { pthread_mutex_lock(&queue->mutex); while (queue->size == MAX_QUEUE_SIZE) { pthread_cond_wait(&queue->not_full, &queue->mutex); } queue->rear = (queue->rear + 1) % MAX_QUEUE_SIZE; strncpy(queue->items[queue->rear].name, name, MAX_NAME_LEN - 1); strncpy(queue->items[queue->rear].path, path, MAX_NAME_LEN - 1); queue->items[queue->rear].img = img; queue->size++; pthread_cond_signal(&queue->not_empty); pthread_mutex_unlock(&queue->mutex); return 0; } 内存管理： name 和 path 使用 strncpy 复制到固定大小的字符数组，MAX_NAME_LEN - 1 确保留空间给终止符，避免缓冲区溢出。 img 指针直接赋值，队列不负责分配或释放 c_mat_t 或其 mat_data，该操作由创建实际参数时负责。 注意：笔者调用前已经确保 img 和 mat_data 在入队期间保持有效，且在队列使用期间不被释放。 出队图像: int dequeue(ImageQueue *queue, ImageInfo *info) { pthread_mutex_lock(&queue->mutex); while (queue->size == 0) { pthread_cond_wait(&queue->not_empty, &queue->mutex); } *info = queue->items[queue->front]; queue->front = (queue->front + 1) % MAX_QUEUE_SIZE; queue->size--; pthread_cond_signal(&queue->not_full); pthread_mutex_unlock(&queue->mutex); return 0; } 内存管理： info 接收 ImageInfo 的副本，name 和 path 的字符数组被复制，img 指针被复制。 注意：出队后，info->img 指向的 c_mat_t 和 mat_data 仍由调用者管理。此时笔者通常会才使用完ImageInfo *info后（如 AI 推理）手动释放，如下所示：（这样的好处在于只是用该张图片的原来存储的堆空间，便于内存管理，以及避免深拷贝带来的时间成本） if (info.img->mat_data) { image_free(info.img); info.img->mat_data = NULL; } 3. 内存管理注意事项 3.1 c_mat_t 和 mat_data 的生命周期 c_mat_t和其 mat_data 由调用者分配和释放，队列仅存储指针。这要求调用者在入队和出队时确保内存的有效性。这意味着调用者需要在调用入队前分配，出队后释放，这一点很重要。 3.2 队列销毁时的内存清理： destroy_queue 仅释放 POSIX 资源，未清理 items 中残留的 img 指针。 问题：如果队列销毁时仍有图像未出队，c_mat_t 和 mat_data 可能泄漏。 改进办法： 在 destroy_queue 中遍历 items，释放有效的 img 和 mat_data（需调用者提供释放函数）。 或者记录所有入队的 c_mat_t，在销毁时统一释放。 4. 总结 该图像缓冲队列通过固定大小的循环缓冲区和线程同步机制，实现了高效的图像数据管理。然而，内存管理的细节至关重要，尤其是在 c_mat_t 和 mat_data 的生命周期管理、字符数组截断、以及队列销毁时的清理等方面。遵循上述注意事项和优化建议，可以显著提高队列的稳定性和可维护性。
C 语言应用开发中的注意事项 1.全局变量 2.源代码文件夹中 .c 模块文件的划分 3.多线程编程注意的问题 线程安全和互斥锁 线程间通信 4.深浅拷贝问题 总结 C 语言应用开发中的注意事项 C 语言作为一门经典的编程语言，广泛应用于系统编程、嵌入式开发和高性能计算等领域。然而，在进行 C 语言应用开发时，开发者需要关注一系列问题，以确保代码的正确性、性能和可维护性。 笔者在基于 C 语言开发 EdgeX Foundry 设备服务实践之中，遇到了一些问题，如全局变量的使用、.c 模块文件的划分、多线程编程注意事项、深浅拷贝问题以及编译链接时的多重定义问题。本文将对其展开讨论，并提供具体的代码示例和实用建议。 1.全局变量 全局变量是在函数外部定义的变量，其作用域从定义处开始到文件结束。它们在程序整个生命周期内存在，通常不会被轻易时放掉。可被程序中的任何函数访问。它能够方便多个函数共享数据，并减少函数参数传递的开销，但同时也不利于模块化编程和代码重用，增加调试难度。 以下展示的 global_pp 全局变量，在 EdgeX Foundry 设备服务中用于保存设备协议的详细信息，为部分关键代码： #include "device_openvino.h" #include "circular_buffer.h" /* protocols */ typedef struct { char *input_model; char *input_image_path; /* Uri */ char *device_name; /* CPU, GPU[0,1,...], NPU */ char *Score; } protocol_properties; protocol_properties *global_pp; // malloc a protocol_properties void func() { if (!global_pp) { iot_log_debug(driver->lc, "malloc a global_pp (protocol_properties)"); global_pp = (protocol_properties *)malloc(sizeof(protocol_properties)); global_pp->input_model = NULL; global_pp->input_image_path = NULL; global_pp->device_name = NULL; global_pp->Score = NULL; } } int main() { func(); return 0; } 注意事项： 尽量减少全局变量的使用，优先考虑局部变量或函数参数。 使用有意义的命名（如 g_config），避免冲突。 在多线程环境中，使用锁机制确保线程安全。 2.源代码文件夹中 .c 模块文件的划分 模块化编程将程序分解为独立的模块，每个模块负责特定功能。这种方法能提高代码的可读性、可维护性和可重用性，同时便于团队协作开发。划分 .c 文件的指导思想有以下几点： 每个 .c 文件应包含一组相关功能的实现，例如字符串处理或网络通信。 避免单个 .c 文件过大，建议控制在 1000 行以内。 使用头文件 (.h) 声明对外接口，.c 文件实现具体逻辑。 头文件用于声明函数、宏、类型和全局变量，供其他模块使用。 以下是 EdgeX Foundry 设备服务中环形 buffer 缓冲区的模块设计： //circular_buffer.h #pragma once #include <stddef.h> // 包含 size_t 的定义 // Circular buffer structure struct circular_buffer { struct infer_result* buffer; // storing data size_t capacity; // Maximum buffer capacity size_t size; // The current number of stored elements size_t head; // Head index (write location) size_t tail; // Tail index (read position) }; struct circular_buffer* create_circular_buffer(size_t capacity); ... //circular_buffer.c #include "circular_buffer.h" #include <stdlib.h> #include "edgex_common.h" // Initialize circular buffer struct circular_buffer* create_circular_buffer(size_t capacity) { //malloc a circular_buffer struct circular_buffer* cb = (struct circular_buffer*)malloc(sizeof(struct circular_buffer)); if (cb == NULL) { return NULL; } cb->buffer = (struct infer_result*)malloc(sizeof(struct infer_result) * capacity); if (cb->buffer == NULL) { free(cb); return NULL; } //Init cb->capacity = capacity; cb->size = 0; cb->head = 0; cb->tail = 0; return cb; } 要点： 使用 #ifndef 和 #define 或 #pragma once 防止头文件重复包含。 头文件仅声明，不定义变量或函数。 保持头文件简洁，避免引入不必要的依赖。 3.多线程编程注意的问题 线程安全和互斥锁 在多线程程序中，多个线程可能同时访问共享资源，导致数据不一致。互斥锁（mutex）可用于保护临界区，确保同一时间只有一个线程操作共享数据。在 EdgeX Foundry 设备服务 SDK 设计中，服务实例结构体包含有互斥锁指针，在设备服务初始化时一并初始化 mutex 以便于在整个设备服务声明周期中进行资源同步管理： /** * @brief Structure representing an OpenVINO driver instance for managing inference operations. */ typedef struct openvino_driver { iot_logger_t *lc; pthread_mutex_t mutex; // for synchronization pthread_t inference_thread; // 推理线程句柄 bool stop_inference; // 控制推理线程停止的标志 } openvino_driver; viod *openvino_inference_thread (){ ... // Thread-safe storage of results pthread_mutex_lock(&driver->mutex); push_result(global_cb, *results); pthread_mutex_unlock(&driver->mutex); } 线程间通信 线程间可通过共享内存、信号量或消息队列通信。例如，使用条件变量实现生产者-消费者模型。在 EdgeX Foundry 设备服务的后续开发中也会频繁出现。 4.深浅拷贝问题 浅拷贝： 只复制基本数据类型和指针，指针指向的内存不复制。 深拷贝： 复制所有数据，包括指针指向的内存。 在 EdgeX Foundry 设备服务开发中，协议资源 global_pp; 在 openvino_create_addr 中由深拷贝赋值，以此避免global_pp;在 SDK 回调函数中被错误释放资源： /** * @brief Creates an address structure for the OpenVINO device service. * @details This callback function is used by the EdgeX Foundry framework to create and populate a protocol_properties * structure with OpenVINO-specific configuration data extracted from the provided protocols. * @param impl Pointer to the implementation-specific data (cast to openvino_driver). * @param protocols Pointer to the protocol data containing OpenVINO configuration. * @param exception Pointer to store any exception data (not used in this implementation). * @return devsdk_address_t A pointer to the populated protocol_properties structure, cast to the EdgeX address type. */ static devsdk_address_t openvino_create_addr(void *impl, const devsdk_protocols *protocols, iot_data_t **exception) { ... const iot_data_t *props = devsdk_protocols_properties(protocols, "Openvino"); //从设备获取协议资源 if (props) { result = iot_data_string_map_get_string(props, "Uri"); pp->input_image_path = result; result = iot_data_string_map_get_string(props, "model"); pp->input_model = result; result = iot_data_string_map_get_string(props, "CPU_GPU"); pp->device_name = result; result = iot_data_string_map_get_string(props, "Score"); pp->Score = result; } // Allocate and populate new global protocol properties // 深拷贝 global_pp->input_model = malloc(strlen(pp->input_model) + 1); strcpy(global_pp->input_model,pp->input_model); global_pp->input_image_path = malloc(strlen(pp->input_image_path) + 1); strcpy(global_pp->input_image_path,pp->input_image_path); global_pp->device_name = malloc(strlen(pp->device_name) + 1); strcpy(global_pp->device_name,pp->device_name); global_pp->Score = malloc(strlen(pp->Score) + 1); strcpy(global_pp->Score,pp->Score); return (devsdk_address_t)pp; } 深拷贝的原因： 对象包含动态分配的内存。 需要独立副本，避免共享内存导致的问题。 总结 在 C 语言应用开发中，开发者需要综合考虑全局变量的合理使用、模块化设计的规范性、多线程编程的线程安全性、深浅拷贝的适用场景以及编译链接的正确性。 通过谨慎使用全局变量、合理划分模块、确保线程安全、正确处理拷贝和避免多重定义问题，可以编写出健壮、高效且易于维护的 C 语言程序。
GCC 链接顺序问题总结 1.问题描述 2.根本原因 3.解决方法 4.注意事项 5.总结 GCC 链接顺序问题总结 1.问题描述 在编译 C/C++ 程序时，使用 GCC 链接库时可能会遇到 undefined reference to ... 的错误，即使正确指定了库（例如 -lopenvino_c）。例如，命令： gcc -I/usr/include -L/usr/lib -lopenvino_c -o main main.c 可能会报错： /usr/bin/ld: main.c:(.text+0x2a): undefined reference to `ov_get_openvino_version' 原因是 GCC 的链接顺序不符合要求。 2.根本原因 GCC 的链接器行为：GCC 使用 ld 作为链接器，ld 在解析符号时是单向扫描的，从左到右处理输入文件和库。 符号解析规则： 链接器在遇到目标文件（如 main.c 编译生成的 .o 文件）时，会记录其中未定义的符号（例如 ov_get_openvino_version）。 只有在后续扫描到的库文件中找到这些符号的定义，链接器才会解析成功。 如果库（-lXXX）出现在源文件或目标文件之前，链接器在处理库时还不知道需要哪些符号，因此不会从中提取定义，导致符号未解析。 正确的链接顺序 规则：GCC 要求源文件或目标文件在前，依赖库在后。 正确示例： gcc -I/usr/include/openvino -L/usr/lib -o main main.c -lopenvino_c ​ - main.c 在前：链接器先处理 main.c，记录未定义的符号。 ​ - -lopenvino_c 在后：链接器随后扫描 libopenvino_c.so，找到符号定义并完成链接。 3.解决方法 调整命令顺序： 始终将源文件（main.c）或目标文件（main.o）放在依赖库（-lXXX）之前。 标准格式：gcc [编译选项] 源文件 -l库名 [链接选项]。 验证符号： 使用 nm -D /path/to/lib.so | grep 函数名 检查库中是否包含所需符号，确保问题确实出在顺序而非库本身。 完整示例： gcc -I/usr/include/openvino -L/usr/lib -o main main.c -lopenvino_c 4.注意事项 多文件情况：如果有多个源文件或目标文件，确保它们都在库之前，例如： gcc -o main file1.c file2.c -lopenvino_c 库之间的依赖：如果库之间有依赖关系，也需要按从被依赖到依赖的顺序排列，例如： gcc -o main main.c -lopenvino_c -lopenvino CMake 自动处理：在使用 CMake 时，链接顺序通常由 target_link_libraries 自动管理，无需手动调整。 5.总结 GCC 的链接顺序要求是“源文件在前，依赖库在后”，这是由链接器 ld 的单向扫描机制决定的。遵循这个规则可以避免 undefined reference 错误，确保符号正确解析。
如何移植 Kionix KXTJ3 系列加速度传感器到嵌入式设备？
3轴加速度，Tri-Axis Accelerometer，G-sensor，有不同的说法，但是表达的意思是一样的，我们要实现物体移动检测，计步器，简单应用还是非常不错的，至于6轴或9轴，不在本文章覆盖范围。 准备工作 参考驱动 官方并没有给出完整的代码参考，不过在github找到了非常不错的示例，只不过代码是 linux 下的，需要移植到嵌入式需要一些工作。仓库地址：https://github.com/eclipse/upm ，完整的驱动在 /src/kxtj3 目录下，需要3个文件：kxtj3.c, kxtj3.h, kxtj3_registers.h。有一个 example 在 /examples/c/kxtj3.c 下。全部移植到你的工程代码目录中。 代码移植 example 文件分析 主要是根据自己的嵌入式平台，改一下适配。 #define SENSOR_ADDR 0x0f // 设备slave号，接口低电平是 0x0E，高电平是 0x0F #define I2C_BUS 0 // 使用哪个 i2c #define SAMPLE_COUNT 10 // 读取传感器次数 bool isStopped = false; void signal_int_handler(int signo) { if (signo == SIGINT) isStopped = true; } // 读取，打印 void print_acceleration_data(kxtj3_context dev) { float wait_time = kxtj3_get_acceleration_sampling_period(dev) * SECOND_IN_MICRO_S; uint8_t sample_counter = 0; float x, y, z; while (sample_counter < SAMPLE_COUNT && !isStopped) { kxtj3_get_acceleration_data(dev, &x, &y, &z); printf("%.02f | %.02f | %.02f\n", x, y, z); usleep(wait_time); sample_counter++; } } int main(int argc, char **argv) { signal(SIGINT, signal_int_handler); // i2c 初始化 printf("Sensor init\n"); kxtj3_context dev = kxtj3_init(I2C_BUS, SENSOR_ADDR); if (!dev) { printf("kxtj3_init() failed.\n"); return -1; } // sensor 初始化 printf("Setting settings:\nODR: 25 Hz\nResolution: High\nAcceleration range: 16g with 14bits"); kxtj3_sensor_init(dev, KXTJ3_ODR_25, HIGH_RES, KXTJ3_RANGE_16G_14); printf("Showing acceleration data:\n"); print_acceleration_data(dev); printf("Closing sensor\n"); kxtj3_close(dev); return 0; } 驱动文件分析 i2c 驱动相关 主要是 i2c 读取/写入的几个函数适配，其他函数不需要更改。 // Register Read/Write helper functions static upm_result_t kxtj3_read_register(const kxtj3_context dev, uint8_t reg, uint8_t *data) { int value = mraa_i2c_read_byte_data(dev->i2c, reg); // 适配字节读取，注意都是指针传递 if (value == -1) { printf("%s: mraa_i2c_read_byte_data() failed.\n", __FUNCTION__); return UPM_ERROR_OPERATION_FAILED; } *data = (uint8_t)value; return UPM_SUCCESS; } static upm_result_t kxtj3_read_registers(const kxtj3_context dev, uint8_t reg, uint8_t *data, int len) { if (mraa_i2c_read_bytes_data(dev->i2c, reg, data, len) != (int)len) // 适配多字节读取，注意都是这真传递 return UPM_ERROR_OPERATION_FAILED; return UPM_SUCCESS; } static upm_result_t kxtj3_write_register(const kxtj3_context dev, uint8_t reg, uint8_t val) { if (mraa_i2c_write_byte_data(dev->i2c, val, reg) != MRAA_SUCCESS) // 适配字节写入 { printf("%s: mraa_i2c_write_byte_data() failed.\n", __FUNCTION__); return UPM_ERROR_OPERATION_FAILED; } return UPM_SUCCESS; } i2c 连接，其实就是指定你的 i2c 相关内容，可有可无，保留函数结构即可。 static bool kxtj3_check_mraa_i2c_connection(kxtj3_context dev, int bus, uint8_t addr) { if (mraa_init() != MRAA_SUCCESS) { printf("%s: mraa_init() failed.\n", FUNCTION); kxtj3_close(dev); return false; } if (!(dev->i2c = mraa_i2c_init(bus))) { printf("%s: mraa_i2c_init() failed.\n", __FUNCTION__); kxtj3_close(dev); return false; } if (mraa_i2c_address(dev->i2c, addr)) { printf("%s: mraa_i2c_address() failed.\n", __FUNCTION__); kxtj3_close(dev); return false; } return true; } 初始化函数，很重要，这是入口，初始化了 kxtj3_context。 kxtj3_context kxtj3_init(int bus, uint8_t addr) { kxtj3_context dev = (kxtj3_context)malloc(sizeof(struct _kxtj3_context)); if (!dev) return NULL; dev->i2c = NULL; dev->interrupt_pin = NULL; if (!kxtj3_check_mraa_i2c_connection(dev, bus, addr)) // i2c 初始化 return NULL; if (!kxtj3_check_who_am_i(dev)) // 读取设备号，必须返回 0x35 才能行 return NULL; kxtj3_set_default_values(dev); kxtj3_set_odr_wakeup_function(dev, dev->odr_wakeup); kxtj3_sensor_init(dev, dev->odr, dev->res_mode, dev->g_range_mode); return dev; } 中断唤醒MCU相关 自定义一个函数，配置中断触发的高低电平和清除模式，在系统初始化时调用即可。 upm_result_t kxtj3_interrupt_init(const kxtj3_context dev) { kxtj3_enable_wakeup_interrupt(dev); kxtj3_enable_interrupt_pin(dev, ACTIVE_LOW, LATCH_UNTIL_CLEARED); // ACTIVE_LOW：低电平触发，LATCH_UNTIL_CLEARED：手工清除 kxtj3_set_wakeup_threshold_g_value(dev, 0.1); // 0.1 代表了 0.1g 的加速度，0.1g 基本可以使能拿起来就触发 return UPM_SUCCESS; } 实验效果 读出来的效果，基本就是三个指标，xyz坐标轴的状态值，自己打印出来即可； 中断触发唤醒MCU，这个也不好演示，自己试试能不能唤醒MCU。
如何将 蓝牙BLE 设备（YiTHE 人体温度计）通过蓝牙网关（YiGATE）接入 YiCONNECT？
目录 关注我们 关注 ThingsBoard 微信公众号 访问 ThingsBoard 中文社区网站 准备工作 设备和软件相关 数据走向 协议转换 蓝牙网关端 ThingsBoard 端 结果呈现 设备列表 设备日志 关注我们 关注 ThingsBoard 微信公众号 手动增加微信公众号：thingsboard_cn 访问 ThingsBoard 中文社区网站 访问网址：http://www.thingsboard.club 我们都知道蓝牙BLE设备只能与蓝牙相关设备通信，如果需要将蓝牙BLE设备接入物联网平台，那需要通过一些技术手段来实现协议和数据的转换。本文章，就是为这个解决方案提供一个指引。 准备工作 设备和软件相关 蓝牙BLE广播设备，可以是一个iBeacon，这里用 YiTHE 人体温度计为例； 蓝牙BLE网关，可以是一个通用的蓝牙网关或可以二次开发的网关； ThingsBoard Gateway，需要二次开发； 蓝牙网关需要跟蓝牙设备之间已经建立连接和配合，具体配置要看蓝牙设备的协议是否被网关支持。 数据走向 蓝牙设备持续广播，将数据发送出来：02010613ff11aae6000055444757543035000000000000； 网关设备持续扫描广播，并解析广播数据，比如 YiTHE ； 蓝牙网关将数据发送到 ThingsBoard Gateway； ThingsBoard 端需要针对 iBeacon 数据解析，并入库。 协议转换 蓝牙网关端 为了区分自有设备，过滤掉其他厂家设备，需要在网关处开发过滤规则： 根据广播数据长度过滤不相关数据； 根据数据内容关键字过滤； ThingsBoard 端 为了保证数据完整性，我们将原始数据直接发送到 TB ，然后，在规则链 中过滤并且分析数据，写入： adv_data = msg.adv_data; // 电池 battery_str = adv_data.substr(20, 2); hex = "0x" + battery_str; charValueBattery = String.fromCharCode(hex); battery = charValueBattery.charCodeAt(0); msg.battery = battery; // 温度 temp_symbol = adv_data.substr(10, 2); if (temp_symbol == "00") { symbol = -1; } else if (temp_symbol == "11") { symbol = 1; } temp1_str = adv_data.substr(16, 2); temp2_str = adv_data.substr(14, 2); hex = "0x" + temp1_str + temp2_str; charValueTemp = String.fromCharCode(hex); temp = charValueTemp.charCodeAt(0); msg.temp = symbol * temp / 10.0; // 温度单位 摄氏度或华氏度 temp_unit_str = adv_data.substr(12, 2); if (temp_unit_str == "aa") { unitCode = "C"; } else if (temp_unit_str == "bb") { unitCode = "F"; } msg.temp_unit = unitCode; // 按键标志 pressed_str = adv_data.substr(18, 2); if (pressed_str == "01") { pressed = true; } else if (pressed_str == "00") { pressed = false; } msg.pressed = pressed; // 设备id devcie_id_str = adv_data.substr(34, 12); hex = "0x" + devcie_id_str; charValueDeviceId = String.fromCharCode(hex); device_id = charValueDeviceId.charCodeAt(0); msg.device_id = device_id; // 设备标志位 var result = []; device_flag_str = adv_data.substr(22, 12); for (var i = 0; i < device_flag_str.length / 2; i++) { var aa = device_flag_str.slice(i * 2, (i + 1) * 2); result.push(aa); } var device_flag = ""; for (var i = 0; i < result.length; i++) { device_flag += String.fromCharCode(parseInt(result[i], 16)); } msg.device_flag = device_flag; return { msg: msg, metadata: metadata, msgType: msgType }; 需要解析数据，并写入msg，达到入库的作用。 { "msg": { "adv_data": "02010613ff11aae6000055444757543035000000000000", "battery": 85, "temp": 23, "temp_unit": "C", "pressed": false, "device_id": 0, "device_flag": "DGWT05" }, "metadata": { "deviceType": "default", "deviceName": "Test Device", "ts": "1618149679239" }, "msgType": "POST_TELEMETRY_REQUEST" } 结果呈现 设备列表 设备日志
软件平台演示 产品特性 无线远传：NB-IoT全网通 干电池供电：2节AA电池（5号） 静态功耗：< 20uA，可用1年* 温/湿度精度：±0.2℃/±3% 私有化 IoT 平台：多租户管理 尺寸：86 x 86 x 30mm 适用范围 供暖、冷链、厂房、仓库 办公室、会议室、学校、幼儿园 酒店、医院、商场、咖啡厅 酒吧、餐馆、地铁、地下室 多租户 仪表板
Google Protocol Buffers ，一个非常高效的数据序列化工具，ava/Python/C++/C#等都是完美支持，可惜了，官方不支持C语言，还好世界强大，有人做了C语言版本：https://github.com/protobuf-c/protobuf-c 关于protobuf，请自行google或百度查询，这里不再啰嗦，进入正题。 1. 第一步：安装Protocol Buffers https://developers.google.com/protocol-buffers/ 很简单，download下来，编译即可；make && make install 2. 第二步：安装protobuf-c https://github.com/protobuf-c/protobuf-c 也是很简单，download下来，编译即可；make && make install 当你输入 protoc-c 出现以下信息，代表安装成功。 jiekechoo:mqtt_protoc jiekechoo$ protoc-c Usage: protoc-c [OPTION] PROTO_FILES Parse PROTO_FILES and generate output based on the options given: -IPATH, --proto_path=PATH Specify the directory in which to search for imports. May be specified multiple times; directories will be searched in order. If not given, the current working directory is used. If not found in any of the these directories, the --descriptor_set_in descriptors will be checked for required proto file. --version Show version info and exit. -h, --help Show this text and exit. --encode=MESSAGE_TYPE Read a text-format message of the given type from standard input and write it in binary to standard output. The message type must be defined in PROTO_FILES or their imports. --decode=MESSAGE_TYPE Read a binary message of the given type from standard input and write it in text format to standard output. The message type must be defined in PROTO_FILES or their imports. --decode_raw Read an arbitrary protocol message from standard input and write the raw tag/value pairs in text format to standard output. No PROTO_FILES should be given when using this flag. --descriptor_set_in=FILES Specifies a delimited list of FILES each containing a FileDescriptorSet (a protocol buffer defined in descriptor.proto). The FileDescriptor for each of the PROTO_FILES provided will be loaded from these FileDescriptorSets. If a FileDescriptor appears multiple times, the first occurrence will be used. -oFILE, Writes a FileDescriptorSet (a protocol buffer, --descriptor_set_out=FILE defined in descriptor.proto) containing all of the input files to FILE. --include_imports When using --descriptor_set_out, also include all dependencies of the input files in the set, so that the set is self-contained. --include_source_info When using --descriptor_set_out, do not strip SourceCodeInfo from the FileDescriptorProto. This results in vastly larger descriptors that include information about the original location of each decl in the source file as well as surrounding comments. --dependency_out=FILE Write a dependency output file in the format expected by make. This writes the transitive set of input file paths to FILE --error_format=FORMAT Set the format in which to print errors. FORMAT may be 'gcc' (the default) or 'msvs' (Microsoft Visual Studio format). --print_free_field_numbers Print the free field numbers of the messages defined in the given proto files. Groups share the same field number space with the parent message. Extension ranges are counted as occupied fields numbers. --c_out=OUT_DIR Generate C/H files. @<filename> Read options and filenames from file. If a relative file path is specified, the file will be searched in the working directory. The --proto_path option will not affect how this argument file is searched. Content of the file will be expanded in the position of @<filename> as in the argument list. Note that shell expansion is not applied to the content of the file (i.e., you cannot use quotes, wildcards, escapes, commands, etc.). Each line corresponds to a single argument, even if it contains spaces. 3. 测试protobuf 转换proto文件为 C 语言库 protoc-c --c_out=. kurapayload.proto 生成protoc文件 -rw-r--r-- 1 jiekechoo staff 14836 8 18 11:36 kurapayload.pb-c.c -rw-r--r-- 1 jiekechoo staff 6427 8 18 11:36 kurapayload.pb-c.h -rw-r--r-- 1 jiekechoo staff 1749 8 18 11:17 kurapayload.proto 完成protobuf集成工作 #include <stdio.h> #include <stdlib.h> #include "MQTTClient.h" #include "kurapayload.pb-c.h" void main() { MQTTClient client; MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; MQTTClient_message pubmsg = MQTTClient_message_initializer; MQTTClient_deliveryToken token; int rc; MQTTClient_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL); conn_opts.keepAliveInterval = 20; conn_opts.cleansession = 1; conn_opts.username = "username"; conn_opts.password = "password"; if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS) { printf("Failed to connect, return code %d\n", rc); exit(-1); } Kuradatatypes__KuraPayload payload = KURADATATYPES__KURA_PAYLOAD__INIT; Kuradatatypes__KuraPayload__KuraMetric **metrics; Kuradatatypes__KuraPayload__KuraPosition position = KURADATATYPES__KURA_PAYLOAD__KURA_POSITION__INIT; position.latitude = 1.1; position.longitude = 1.1; payload.position = &position; metrics = malloc(sizeof (Kuradatatypes__KuraPayload__KuraMetric) * 2); int i = 0; for(i=0;i<2;i++) { metrics[i] = malloc (sizeof (Kuradatatypes__KuraPayload__KuraMetric)); kuradatatypes__kura_payload__kura_metric__init(metrics[i]); metrics[i]->name = (char*)EdcBirthPayloadNames[i]; printf("%s, ", metrics[i]->name); metrics[i]->type = KURADATATYPES__KURA_PAYLOAD__KURA_METRIC__VALUE_TYPE__STRING; metrics[i]->string_value = (char*)EdcBirthPayloadValues[i]; printf("%s\r\n", metrics[i]->string_value); } payload.n_metric = 2; payload.metric = metrics; size_t len = kuradatatypes__kura_payload__get_packed_size(&payload); void* data = malloc(len); kuradatatypes__kura_payload__pack(&payload, data); pubmsg.payload = data; pubmsg.payloadlen = kuradatatypes__kura_payload__get_packed_size(&payload); MQTTClient_publishMessage(client, BIRTH_TOPIC, &pubmsg, &token); } 编译，如果没有出错就上成功。 gcc kurapayload.pb-c.c main.c -lprotobuf-c 4. 注意事项 系统要能够使用 malloc 功能，好多地方要用到； 结构体初始化 INIT 或 使用 init 函数； 数据准备完毕，要 pack 一下，再 packed_size 计算长度，后面该怎么处理都行了； 最终，C 语言版本的 Kura 模拟器成功连接 Kapua。
GD32 兆易创新单片机，国产芯，必须支持！完美兼容 ARM 系列其他产品，价格美丽，性能卓越，支持到位。 freeModbus，开源且免费的 Modbus slave 实现，支持各种平台，包括嵌入式单片机系统。 1. 准备 GD32 系列单片机设备，本文采用 GD32F130F8； 你需要一块带有 GD32 单片机的板子，可以说开发板或成品设备，电路正常，带有串口 USART，最好具备 RS485 电路；用官方的 GD32F1x0_Firmware_Library_V3.0.0 example 新建项目，编译成功，且下载到设备，能够运行跑马灯，串口能打印正常。 串口输出正常，波特率正常，比如 USART0 串口中断输入正常，收到外部串口输入能及时处理 定时器TIMER正常，需要 us 微秒级控制 GPIO输出正常，能够控制高低电平 2、准备 freeModbus V1.6版本，V1.5 也可以； 作为开发者移植 Modbus，相关原理最好还是要熟悉一下：https://www.modbus.org/, 不管怎么样，通过你自己的方式熟读一下 Modbus 协议内容。 github下载 freeModbus， 给作者 star，fork 到自己的项目； modbus 目录结构 原封不动，搬进你的工程中 ├── ascii // ASCII模式文件，本例程未用到 │ ├── mbascii.c │ └── mbascii.h ├── functions // 功能函数模块，很重要，不需要修改 │ ├── mbfunccoils.c │ ├── mbfuncdiag.c │ ├── mbfuncdisc.c │ ├── mbfuncholding.c │ ├── mbfuncinput.c │ ├── mbfuncother.c │ └── mbutils.c ├── include // 头文件，很重要，不需要修改 │ ├── mb.h │ ├── mbconfig.h │ ├── mbframe.h │ ├── mbfunc.h │ ├── mbport.h │ ├── mbproto.h │ └── mbutils.h ├── mb.c // 主文件，不需要修改 ├── rtu // RTU 模块文件，很重要，本例程需要熟读内容 │ ├── mbcrc.c │ ├── mbcrc.h │ ├── mbrtu.c │ └── mbrtu.h └── tcp // TCP 模块，本例程未用到 ├── mbtcp.c └── mbtcp.h 移植要求 主要是针对 port 目录下的移植 ├── demo.c // 主函数移植内容 └── port // 移植目录 ├── port.h // 头文件 ├── portevent.c // 移植事件文件，本例程不修改 ├── portserial.c // 移植串口处理，很重要，需要熟读 └── porttimer.c // 移植定时器处理，很重要，需要熟读 3、移植过程； main.c 使用上一步中的 demo/BARE/port 下所有文件导入 GD32 工程中，且加入所有的 .h 文件到编译目录；将 demo.c 中的主循环内容移植到你的工程主循环中，波特率，串口号，等等参数，自行根据需要修改。 eMBErrorCode eStatus; eStatus = eMBInit( MB_RTU, 0x03, 0, 9600, MB_PAR_NONE ); /* Enable the Modbus Protocol Stack. */ eStatus = eMBEnable( ); for( ;; ) { ( void )eMBPoll( ); } port.c 在工程中新建一个 port.c ， 把 modbus 回调函数填入，这里只是主要内容，其他默认回调也必须存在 /* ----------------------- Defines ------------------------------------------*/ #define REG_HOLDING_START ( 0x0000 ) #define REG_HOLDING_NREGS ( 4 ) /* ----------------------- Static variables ---------------------------------*/ static USHORT usRegHoldingStart = REG_HOLDING_START; static USHORT usRegHoldingBuf[REG_HOLDING_NREGS] = { 0xAAAA, 0xBBBB, 0xCCCC, 0xDDDD}; eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode ) { /* error state */ eMBErrorCode eStatus = MB_ENOERR; /* offset */ int16_t iRegIndex; /* test if the reg is in the range */ if (((int16_t)usAddress-1 >= REG_HOLDING_START) && (usAddress-1 + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS)) { /* compute the reg's offset */ iRegIndex = (int16_t)(usAddress-1 - REG_HOLDING_START); switch (eMode) { case MB_REG_READ: while (usNRegs > 0) { *pucRegBuffer++ = (uint8_t)( usRegHoldingBuf[iRegIndex] >> 8 ); *pucRegBuffer++ = (uint8_t)( usRegHoldingBuf[iRegIndex] & 0xff); iRegIndex ++; usNRegs --; } break; case MB_REG_WRITE: while (usNRegs > 0) { usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8; usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++; iRegIndex ++; usNRegs --; } break; } } else{ eStatus = MB_ENOREG; } return eStatus; } 增加全局中断函数使能/失能调用，这里用 CMSIS 通用配置 void EnterCriticalSection( void ) { __disable_irq(); } void ExitCriticalSection( void ) { __enable_irq(); } portserial.c 串口使能/失能配置【很重要】 /* ----------------------- Start implementation -----------------------------*/ void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable ) { /* If xRXEnable enable serial receive interrupts. If xTxENable enable * transmitter empty interrupts. */ if (xRxEnable) { gd_eval_ledoff(LED3); // 输入模式，设置 RS485 芯片 RE 口低电平 usart_interrupt_enable(EVAL_COM1, USART_INT_RBNEIE); // 打开输入中断 } else { gd_eval_ledon(LED3); // 输出模式，设置 RS485 芯片 RE 口低电平 usart_interrupt_disable(EVAL_COM1, USART_INT_RBNEIE); // 关闭输入中断 } if(xTxEnable) { usart_interrupt_enable(EVAL_COM1, USART_INT_TCIE); // 打开输出完成中断 } else { usart_interrupt_disable(EVAL_COM1, USART_INT_TCIE); // 关闭输出完成中断 } } 串口初始化配置 BOOL xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity ) { /* USART interrupt configuration */ nvic_irq_enable(USART0_IRQn, 0, 2); // 串口中断号设置 gd_eval_COMinit(EVAL_COM1); // 初始化串口 usart_interrupt_enable(EVAL_COM1, USART_INT_RBNEIE); // 初始化后好就打开串口输入中断 gd_eval_ledoff(LED3); // 485 芯片 RE 口低电平输入模式 return TRUE; } 串口输出/输入单字节操作【很重要，也很简单】 BOOL xMBPortSerialPutByte( CHAR ucByte ) { /* Put a byte in the UARTs transmit buffer. This function is called * by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been * called. */ usart_data_transmit(EVAL_COM1, ucByte); return TRUE; } BOOL xMBPortSerialGetByte( CHAR * pucByte ) { /* Return the byte in the UARTs receive buffer. This function is called * by the protocol stack after pxMBFrameCBByteReceived( ) has been called. */ *pucByte = usart_data_receive(EVAL_COM1); return TRUE; } 串口中断回调函数【非常重要，非常重要，非常重要】 void USART0_IRQHandler(void) { if(RESET != usart_interrupt_flag_get(EVAL_COM1, USART_STAT_RBNE, USART_INT_RBNEIE)){ /* receive data */ prvvUARTRxISR(); } if(RESET != usart_flag_get(EVAL_COM1, USART_STAT_TC)){ /* transmit data */ prvvUARTTxReadyISR(); } } 其他内容不需要修改。 porttimer.c 此文件主要是定时器设置，关于这个定时器，很多文章有写到，最关键是 50us 这个要相对准确 定时器初始化，说多了都是泪，自己体会，照抄下面的代码即可，注意使用哪个定时器TIMER1，MCU 主频要换算成 50us BOOL xMBPortTimersInit( USHORT usTim1Timerout50us ) { timer_parameter_struct timer_initpara; rcu_periph_clock_enable(RCU_TIMER1); timer_deinit(TIMER1); /* TIMER1 configuration */ timer_initpara.timer_prescaler = 3599; // 72MHz, 注意，这样就是50us timer_initpara.timer_alignedmode = TIMER_COUNTER_EDGE; timer_initpara.timer_counterdirection = TIMER_COUNTER_UP; timer_initpara.timer_period = usTim1Timerout50us; timer_initpara.timer_clockdivision = TIMER_CKDIV_DIV1; timer_initpara.timer_repetitioncounter = 0; timer_init(TIMER1,&timer_initpara); /* TIMER0 channel control update interrupt enable */ timer_interrupt_enable(TIMER1,TIMER_INT_UP); /* TIMER1 counter enable */ timer_enable(TIMER1); // NVIC CONFIG nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3); nvic_irq_enable(TIMER1_IRQn, 1, 1); return TRUE; } 定时器使能/失能配置【很简单，但是很重要，尤其是计数器要清零】 inline void vMBPortTimersEnable( ) { /* Enable the timer with the timeout passed to xMBPortTimersInit( ) */ timer_interrupt_flag_clear(TIMER1,TIMER_INT_UP); timer_counter_value_config(TIMER1, 0); // 清零 /* TIMER1 counter enable */ timer_enable(TIMER1); } inline void vMBPortTimersDisable( ) { /* Disable any pending timers. */ timer_counter_value_config(TIMER1, 0); // 清零 /* TIMER1 counter enable */ timer_disable(TIMER1); } 定时器中断回调函数【很重要，但是很简单】 void TIMER1_IRQHandler(void) { prvvTIMERExpiredISR(); timer_interrupt_flag_clear(TIMER1,TIMER_INT_UP); // 清除定时器中断标志位 } 定时器其他照旧即可 portevent.c 没什么修改的 port.h 需要特别注意，修改下面两行 #define ENTER_CRITICAL_SECTION( ) EnterCriticalSection() #define EXIT_CRITICAL_SECTION( ) ExitCriticalSection() 4、结果验证； 本例程只实现了 RTU 功能号 03 [保持寄存器读取]，其他功能请自行扩展！ 有任何问题，请留言，我们共同讨论。
GD32 MCU 中的延时函数和非阻塞式定期任务
systick.c 定义1Hz /*! \brief configure systick \param[in] none \param[out] none \retval none */ void systick_config(void) { /* setup systick timer for 1Hz interrupts */ if (SysTick_Config(SystemCoreClock / 1000 / 1000)){ /* capture error */ while (1); } /* configure the systick handler priority */ NVIC_SetPriority(SysTick_IRQn, 0x00); } 阻塞式延时函数 /*! \brief delay a time in milliseconds \param[in] count: count in milliseconds \param[out] none \retval none */ void delay_1us(uint32_t count) { delay = count; while(0 != delay); } void delay_1ms(uint32_t count) { delay = count * 1000; while(0 != delay); } gd32f1x0_it.c 中断处理函数中调用led_spark()，实现led翻转 /*! \brief this function handles SysTick exception \param[in] none \param[out] none \retval none */ void SysTick_Handler(void) { led_spark(); delay_decrement(); } main.c led 翻转函数 /*! \brief toggle the led every 500ms \param[in] none \param[out] none \retval none */ void led_spark(void) { static __IO uint32_t timingdelaylocal = 0; if(timingdelaylocal){ if(timingdelaylocal < 500*1000){ gd_eval_ledon(LED2); }else{ gd_eval_ledoff(LED2); } timingdelaylocal--; }else{ timingdelaylocal = 1000*1000; } } 日常阻塞式翻转 int main(void) { gd_eval_ledinit(LED2); systick_config(); while(1) { gd_eval_ledon(LED2); delay_1us(500*1000); gd_eval_ledoff(LED2); delay_1us(200*1000); } }