亿琪软件-开源博客 - 嵌入式软件
type: channel
link: https://www.yiqisoft.cn/blogs/category/embedded/
items: 基于 C 语言的线程安全的图像缓冲队列
items: C 语言应用开发中的注意事项
items: 分享-GCC 手动编译问题 之 链接顺序
items: 如何移植 Kionix KXTJ3 系列加速度传感器到嵌入式设备？
items: 如何将 蓝牙BLE 设备（YiTHE 人体温度计）通过蓝牙网关（YiGATE）接入 YiCONNECT？
items: 无线远传温湿度计-YiAIR86 预发布
items: Protobuf C 语言支持工具包
items: GD32 移植 freeModbus slave RTU 成功经验分享【原创】
items: GD32 MCU 中的延时函数和非阻塞式定期任务

基于 C 语言的线程安全的图像缓冲队列
type: item
date: 2025-04-20T08:14:30+00:00
description: 线程安全的图像缓冲队列 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 在入队期间保持有效，且在队列使用期…

C 语言应用开发中的注意事项
type: item
date: 2025-04-12T15:55:44+00:00
description: 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_comm…

分享-GCC 手动编译问题 之 链接顺序
type: item
date: 2025-03-23T11:44:40+00:00
description: 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 系列加速度传感器到嵌入式设备？
type: item
date: 2021-09-14T13:12:00+00:00
description: 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_…

如何将 蓝牙BLE 设备（YiTHE 人体温度计）通过蓝牙网关（YiGATE）接入 YiCONNECT？
type: item
date: 2021-04-08T00:39:00+00:00
description: 目录 关注我们 关注 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 = de…

无线远传温湿度计-YiAIR86 预发布
type: item
date: 2020-11-09T16:01:00+00:00
description: 软件平台演示 产品特性 无线远传：NB-IoT全网通 干电池供电：2节AA电池（5号） 静态功耗：< 20uA，可用1年* 温/湿度精度：±0.2℃/±3% 私有化 IoT 平台：多租户管理 尺寸：86 x 86 x 30mm 适用范围 供暖、冷链、厂房、仓库 办公室、会议室、学校、幼儿园 酒店、医院、商场、咖啡厅 酒吧、餐馆、地铁、地下室 多租户 仪表板

Protobuf C 语言支持工具包
type: item
date: 2020-09-04T00:20:00+00:00
description: 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…

GD32 移植 freeModbus slave RTU 成功经验分享【原创】
type: item
date: 2020-07-10T12:55:00+00:00
description: 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…

GD32 MCU 中的延时函数和非阻塞式定期任务
type: item
date: 2020-07-06T09:25:00+00:00
description: 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); } }