驱动开发
分层架构
HAL层 vs LL层
STM32提供两种底层驱动库:
| 特性 | HAL(硬件抽象层) | LL(低层) |
|---|---|---|
| 抽象程度 | 高 | 低 |
| 代码大小 | 大(+50KB) | 小(+10KB) |
| 执行效率 | 慢(函数调用) | 快(内联/宏) |
| 易用性 | 简单 | 需理解寄存器 |
| 移植性 | 好 | 一般 |
| 中断回调 | 支持 | 需手动实现 |
| 适用场景 | 快速开发,功能优先 | 性能优先,资源受限 |
// HAL方式:初始化GPIO
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
// LL方式:初始化GPIO
LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_5, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_5, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_5, LL_GPIO_SPEED_FREQ_HIGH);
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
// 寄存器方式:最高效
GPIOA->MODER |= (1 << 10); // 输出模式
GPIOA->BSRR = (1 << 5); // 置位
驱动分层设计
// 层次结构
// 应用层(App)
// ↓
// 中间件层(Middleware):FreeRTOS, LwIP, FatFS
// ↓
// 驱动层(Driver):外设驱动
// ↓
// HAL/LL层:芯片厂商库
// ↓
// 硬件层(Hardware)
// 示例:传感器驱动分层
// 1. 硬件接口层(移植层)
typedef struct {
void (*i2c_write)(uint8_t addr, uint8_t reg, uint8_t data);
void (*i2c_read)(uint8_t addr, uint8_t reg, uint8_t *data, uint8_t len);
void (*delay_ms)(uint32_t ms);
} BMP280_HW_t;
// 2. 驱动层(通用代码)
typedef struct {
BMP280_HW_t hw;
int32_t temperature;
uint32_t pressure;
} BMP280_t;
void BMP280_Init(BMP280_t *dev, BMP280_HW_t *hw);
void BMP280_ReadData(BMP280_t *dev);
// 3. 应用层
BMP280_t bmp280;
BMP280_HW_t hw = {
.i2c_write = HAL_I2C_Mem_Write_Wrapper,
.i2c_read = HAL_I2C_Mem_Read_Wrapper,
.delay_ms = HAL_Delay
};
BMP280_Init(&bmp280, &hw);
传感器驱动开发
DHT11温湿度传感器
// DHT11:单总线协议,时序敏感
#define DHT11_PIN GPIO_PIN_0
#define DHT11_GPIO GPIOA
// 设置引脚方向
void DHT11_SetOutput(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(DHT11_GPIO, &GPIO_InitStruct);
}
void DHT11_SetInput(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(DHT11_GPIO, &GPIO_InitStruct);
}
// 读取数据
uint8_t DHT11_Read(uint8_t *temp, uint8_t *humi) {
uint8_t data[5] = {0};
// 1. 主机发送起始信号:低电平18ms
DHT11_SetOutput();
HAL_GPIO_WritePin(DHT11_GPIO, DHT11_PIN, GPIO_PIN_RESET);
HAL_Delay(18);
HAL_GPIO_WritePin(DHT11_GPIO, DHT11_PIN, GPIO_PIN_SET);
delay_us(30);
// 2. DHT11响应:低电平80us,高电平80us
DHT11_SetInput();
if (HAL_GPIO_ReadPin(DHT11_GPIO, DHT11_PIN) == GPIO_PIN_RESET) {
while (!HAL_GPIO_ReadPin(DHT11_GPIO, DHT11_PIN)); // 等待变高
while (HAL_GPIO_ReadPin(DHT11_GPIO, DHT11_PIN)); // 等待变低
// 3. 读取40位数据
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 8; j++) {
while (!HAL_GPIO_ReadPin(DHT11_GPIO, DHT11_PIN)); // 等待变高
delay_us(30);
data[i] <<= 1;
if (HAL_GPIO_ReadPin(DHT11_GPIO, DHT11_PIN)) {
data[i] |= 1; // 高电平70us为1,26us为0
}
while (HAL_GPIO_ReadPin(DHT11_GPIO, DHT11_PIN)); // 等待变低
}
}
// 4. 校验
if (data[4] == ((data[0] + data[1] + data[2] + data[3]) & 0xFF)) {
*humi = data[0];
*temp = data[2];
return 1; // 成功
}
}
return 0; // 失败
}
BMP280气压传感器(I2C)
#define BMP280_ADDR 0x76
// 寄存器定义
#define BMP280_REG_ID 0xD0
#define BMP280_REG_CTRL_MEAS 0xF4
#define BMP280_REG_CONFIG 0xF5
#define BMP280_REG_PRESS_MSB 0xF7
#define BMP280_REG_TEMP_MSB 0xFA
// 校准参数
typedef struct {
uint16_t dig_T1;
int16_t dig_T2, dig_T3;
uint16_t dig_P1;
int16_t dig_P2, dig_P3, dig_P4, dig_P5, dig_P6, dig_P7, dig_P8, dig_P9;
} BMP280_Calib_t;
BMP280_Calib_t calib;
int32_t t_fine;
// 初始化
void BMP280_Init(void) {
uint8_t id;
HAL_I2C_Mem_Read(&hi2c1, BMP280_ADDR << 1, BMP280_REG_ID, 1, &id, 1, 100);
if (id != 0x58) {
return; // 错误的设备ID
}
// 读取校准参数
uint8_t calib_data[24];
HAL_I2C_Mem_Read(&hi2c1, BMP280_ADDR << 1, 0x88, 1, calib_data, 24, 100);
calib.dig_T1 = (calib_data[1] << 8) | calib_data[0];
calib.dig_T2 = (calib_data[3] << 8) | calib_data[2];
// ...其他参数
// 配置采样和模式
uint8_t ctrl_meas = 0x27; // 温度×1, 压力×1, 正常模式
HAL_I2C_Mem_Write(&hi2c1, BMP280_ADDR << 1, BMP280_REG_CTRL_MEAS, 1, &ctrl_meas, 1, 100);
}
// 读取温度
int32_t BMP280_ReadTemperature(void) {
uint8_t data[3];
HAL_I2C_Mem_Read(&hi2c1, BMP280_ADDR << 1, BMP280_REG_TEMP_MSB, 1, data, 3, 100);
int32_t adc_T = ((int32_t)data[0] << 12) | ((int32_t)data[1] << 4) | ((int32_t)data[2] >> 4);
// 温度补偿算法(数据手册提供)
int32_t var1, var2;
var1 = ((((adc_T >> 3) - ((int32_t)calib.dig_T1 << 1))) * ((int32_t)calib.dig_T2)) >> 11;
var2 = (((((adc_T >> 4) - ((int32_t)calib.dig_T1)) * ((adc_T >> 4) - ((int32_t)calib.dig_T1))) >> 12) *
((int32_t)calib.dig_T3)) >> 14;
t_fine = var1 + var2;
return (t_fine * 5 + 128) >> 8; // 单位:0.01°C
}
OLED显示驱动(SPI)
// 0.96寸OLED: 128×64像素, SSD1306控制器
#define OLED_CMD 0
#define OLED_DATA 1
// 发送命令/数据
void OLED_Write(uint8_t data, uint8_t cmd) {
HAL_GPIO_WritePin(OLED_DC_GPIO, OLED_DC_PIN, cmd ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(OLED_CS_GPIO, OLED_CS_PIN, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, &data, 1, 100);
HAL_GPIO_WritePin(OLED_CS_GPIO, OLED_CS_PIN, GPIO_PIN_SET);
}
// 初始化
void OLED_Init(void) {
HAL_Delay(100);
OLED_Write(0xAE, OLED_CMD); // 显示关闭
OLED_Write(0x20, OLED_CMD); // 地址模式
OLED_Write(0x02, OLED_CMD); // 页地址模式
OLED_Write(0xB0, OLED_CMD); // 起始页
OLED_Write(0xC8, OLED_CMD); // COM扫描方向
OLED_Write(0x00, OLED_CMD); // 低列地址
OLED_Write(0x10, OLED_CMD); // 高列地址
OLED_Write(0x81, OLED_CMD); // 对比度
OLED_Write(0xFF, OLED_CMD);
OLED_Write(0xAF, OLED_CMD); // 显示开启
}
// 显示字符(8×16字体)
const uint8_t font_8x16[][16] = {
// 'A'
{0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,
0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00},
// ...
};
void OLED_ShowChar(uint8_t x, uint8_t y, char ch) {
uint8_t index = ch - ' ';
OLED_Write(0xB0 + y, OLED_CMD); // 设置页
OLED_Write((x & 0x0F), OLED_CMD); // 低列地址
OLED_Write(((x >> 4) & 0x0F) | 0x10, OLED_CMD); // 高列地址
for (int i = 0; i < 8; i++) {
OLED_Write(font_8x16[index][i], OLED_DATA);
}
OLED_Write(0xB0 + y + 1, OLED_CMD);
OLED_Write((x & 0x0F), OLED_CMD);
OLED_Write(((x >> 4) & 0x0F) | 0x10, OLED_CMD);
for (int i = 8; i < 16; i++) {
OLED_Write(font_8x16[index][i], OLED_DATA);
}
}
W25Q128 Flash驱动(SPI)
// 指令定义
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg 0x05
#define W25X_PageProgram 0x02
#define W25X_ReadData 0x03
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
// 读取状态寄存器
uint8_t W25Q128_ReadSR(void) {
uint8_t cmd = W25X_ReadStatusReg;
uint8_t status;
CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
HAL_SPI_Receive(&hspi1, &status, 1, 100);
CS_HIGH();
return status;
}
// 等待空闲
void W25Q128_WaitBusy(void) {
while ((W25Q128_ReadSR() & 0x01) == 0x01); // BUSY位
}
// 写使能
void W25Q128_WriteEnable(void) {
uint8_t cmd = W25X_WriteEnable;
CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
CS_HIGH();
}
// 扇区擦除(4KB)
void W25Q128_EraseSector(uint32_t addr) {
uint8_t cmd[4];
cmd[0] = W25X_SectorErase;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
W25Q128_WriteEnable();
W25Q128_WaitBusy();
CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, 100);
CS_HIGH();
W25Q128_WaitBusy();
}
// 页编程(256字节)
void W25Q128_PageWrite(uint32_t addr, const uint8_t *data, uint16_t len) {
uint8_t cmd[4];
cmd[0] = W25X_PageProgram;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
W25Q128_WriteEnable();
CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, 100);
HAL_SPI_Transmit(&hspi1, (uint8_t*)data, len, 100);
CS_HIGH();
W25Q128_WaitBusy();
}
// 读取数据
void W25Q128_Read(uint32_t addr, uint8_t *buf, uint16_t len) {
uint8_t cmd[4];
cmd[0] = W25X_ReadData;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, 100);
HAL_SPI_Receive(&hspi1, buf, len, 100);
CS_HIGH();
}
Bootloader设计
分区方案
Flash布局:
0x08000000 ┌──────────────────┐
│ Bootloader │ 16KB
0x08004000 ├──────────────────┤
│ App Partition │ 100KB
0x0801D000 ├──────────────────┤
│ Backup/Update │ 100KB
0x08036000 ├──────────────────┤
│ Config/Params │ 8KB
0x08038000 └──────────────────┘
Bootloader实现
// 固件信息结构(App起始处)
typedef struct {
uint32_t magic; // 0xA55AA55A
uint32_t version; // 版本号
uint32_t size; // 固件大小
uint32_t crc32; // CRC校验
uint32_t timestamp; // 时间戳
} FirmwareInfo_t;
#define APP_ADDR 0x08004000
#define BACKUP_ADDR 0x0801D000
// CRC32计算
uint32_t calc_crc32(const uint8_t *data, uint32_t len) {
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
}
}
return ~crc;
}
// 验证固件
uint8_t verify_firmware(uint32_t addr) {
FirmwareInfo_t *info = (FirmwareInfo_t*)addr;
// 检查magic
if (info->magic != 0xA55AA55A) {
return 0;
}
// 检查CRC
uint32_t crc = calc_crc32((uint8_t*)(addr + sizeof(FirmwareInfo_t)),
info->size);
if (crc != info->crc32) {
return 0;
}
return 1;
}
// Bootloader主函数
int main(void) {
HAL_Init();
SystemClock_Config();
// 检查升级标志
uint32_t update_flag = *((uint32_t*)0x20000000); // RAM标志
if (update_flag == 0x5AA5A55A) {
// 执行升级
if (verify_firmware(BACKUP_ADDR)) {
// 擦除App分区
for (uint32_t addr = APP_ADDR; addr < BACKUP_ADDR; addr += 0x800) {
Flash_ErasePage(addr);
}
// 复制固件
FirmwareInfo_t *info = (FirmwareInfo_t*)BACKUP_ADDR;
uint32_t total_size = sizeof(FirmwareInfo_t) + info->size;
for (uint32_t i = 0; i < total_size; i += 2) {
uint16_t data = *((uint16_t*)(BACKUP_ADDR + i));
Flash_Write(APP_ADDR + i, data);
}
// 清除升级标志
*((uint32_t*)0x20000000) = 0;
}
}
// 验证并跳转到App
if (verify_firmware(APP_ADDR)) {
jump_to_app(APP_ADDR);
} else {
// 固件损坏,进入串口升级模式
uart_update_mode();
}
while (1);
}
串口升级协议
// Ymodem协议简化版
#define SOH 0x01 // 128字节数据块
#define STX 0x02 // 1024字节数据块
#define EOT 0x04 // 传输结束
#define ACK 0x06 // 确认
#define NAK 0x15 // 重传
#define CAN 0x18 // 取消
void uart_update_mode(void) {
uint8_t rx_buf[1024];
uint32_t addr = BACKUP_ADDR;
printf("Waiting for firmware...\n");
while (1) {
uint8_t header = uart_recv_byte();
if (header == SOH || header == STX) {
uint16_t len = (header == SOH) ? 128 : 1024;
uint8_t seq = uart_recv_byte();
uint8_t seq_inv = uart_recv_byte();
uart_recv_bytes(rx_buf, len);
uint16_t crc_recv = (uart_recv_byte() << 8) | uart_recv_byte();
// 验证CRC
uint16_t crc_calc = crc16(rx_buf, len);
if (crc_calc == crc_recv && seq == (uint8_t)~seq_inv) {
// 写入Flash
for (uint16_t i = 0; i < len; i += 2) {
Flash_Write(addr + i, *(uint16_t*)(rx_buf + i));
}
addr += len;
uart_send_byte(ACK); // 确认
} else {
uart_send_byte(NAK); // 请求重传
}
} else if (header == EOT) {
uart_send_byte(ACK);
printf("Update complete, rebooting...\n");
HAL_Delay(100);
// 设置升级标志并复位
*((uint32_t*)0x20000000) = 0x5AA5A55A;
NVIC_SystemReset();
}
}
}
调试技术
printf重定向
// 方法1:重定向到UART
int _write(int file, char *ptr, int len) {
HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY);
return len;
}
// 方法2:使用ITM(Instrumentation Trace Macrocell)
int _write(int file, char *ptr, int len) {
for (int i = 0; i < len; i++) {
ITM_SendChar(ptr[i]);
}
return len;
}
// 方法3:半主机模式(调试时使用,量产需关闭)
// 在链接器选项中添加 --specs=rdimon.specs
断言与错误处理
// 调试断言
#ifdef DEBUG
#define ASSERT(expr) \
if (!(expr)) { \
printf("Assert failed: %s, %s, line %d\n", #expr, __FILE__, __LINE__); \
while(1); \
}
#else
#define ASSERT(expr)
#endif
// 错误处理
typedef enum {
ERR_OK = 0,
ERR_TIMEOUT,
ERR_CRC,
ERR_NO_DEVICE,
ERR_BUSY
} ErrorCode_t;
ErrorCode_t sensor_read(uint8_t *data) {
if (!i2c_ready()) {
return ERR_BUSY;
}
if (i2c_read_timeout(data, 100) != HAL_OK) {
return ERR_TIMEOUT;
}
if (!check_crc(data)) {
return ERR_CRC;
}
return ERR_OK;
}
运行时统计
// FreeRTOS任务统计
void print_task_stats(void) {
char buffer[512];
vTaskList(buffer); // 任务列表
printf("%s\n", buffer);
vTaskGetRunTimeStats(buffer); // CPU使用率
printf("%s\n", buffer);
}
// 自定义性能监控
typedef struct {
uint32_t total_time;
uint32_t max_time;
uint32_t call_count;
} PerfCounter_t;
PerfCounter_t perf_adc;
void measure_adc_performance(void) {
uint32_t start = DWT->CYCCNT; // 使用DWT计数器
adc_read();
uint32_t elapsed = DWT->CYCCNT - start;
perf_adc.total_time += elapsed;
perf_adc.call_count++;
if (elapsed > perf_adc.max_time) {
perf_adc.max_time = elapsed;
}
printf("ADC: avg=%d cycles, max=%d\n",
perf_adc.total_time / perf_adc.call_count,
perf_adc.max_time);
}
代码规范
// 命名规则
#define MAX_BUFFER_SIZE 256 // 宏:全大写,下划线分隔
typedef struct { // 类型:首字母大写,_t结尾
uint8_t id;
uint32_t timestamp;
} SensorData_t;
void sensor_init(void); // 函数:小写,下划线分隔
static uint8_t local_buffer[100]; // 静态变量:小写
uint32_t g_SystemTick = 0; // 全局变量:g_前缀
// 注释规范
/**
* @brief 读取传感器数据
* @param id: 传感器ID (1-10)
* @param data: 数据缓冲区指针
* @param len: 缓冲区长度
* @retval 0:成功, -1:失败
* @note 调用前需先初始化I2C
*/
int8_t sensor_read(uint8_t id, uint8_t *data, uint16_t len);
// 版本管理
#define FW_VERSION_MAJOR 1
#define FW_VERSION_MINOR 2
#define FW_VERSION_PATCH 3
#define FW_VERSION_STRING "v1.2.3"
高频面试题
1. HAL库和寄存器操作如何选择?
答案:
选择原则:
- HAL库: 快速开发,跨平台移植,团队协作
- 寄存器: 性能关键代码,资源受限,深入理解硬件
实践中混合使用:
// 初始化用HAL(方便)
HAL_GPIO_Init(&gpio_init);
// 高频操作用寄存器(高效)
#define LED_ON() (GPIOA->BSRR = (1 << 5))
2. 如何调试I2C/SPI通信问题?
答案:
步骤化排查:
- 硬件检查: 万用表测电压,示波器/逻辑分析仪看波形
- 时序验证: 检查时钟频率,SCL/SDA上拉电阻(I2C需2.2-4.7kΩ)
- 软件调试:
- 读设备ID验证通信
- 检查ACK/NACK
- 确认地址(7位 vs 8位)
- SPI检查CPOL/CPHA模式
常见错误:
- I2C地址错误(datasheet给的是7位,代码需左移1位)
- SPI片选时序不对
- 时钟频率过高(某些传感器<400kHz)
3. Bootloader如何防止升级失败导致设备变砖?
答案:
多重保护机制:
- 双分区: App + Backup,升级失败可回滚
- CRC校验: 下载前后都校验
- 分步操作: 先写Backup,验证通过再覆盖App
- 看门狗: 升级过程定期喂狗
- 掉电保护: 记录升级进度,断电后恢复
typedef enum {
UPDATE_IDLE,
UPDATE_DOWNLOADING,
UPDATE_VERIFYING,
UPDATE_INSTALLING,
UPDATE_COMPLETE
} UpdateState_t;
// 保存在掉电保持的区域
__attribute__((section(".noinit"))) UpdateState_t update_state;
void bootloader_main(void) {
switch (update_state) {
case UPDATE_DOWNLOADING:
// 断电恢复,继续下载
break;
case UPDATE_INSTALLING:
// 安装中断,回滚到备份
rollback_firmware();
break;
// ...
}
}
4. 如何优化驱动的移植性?
答案:
使用硬件抽象层:
// 硬件接口层(需移植)
typedef struct {
void (*delay_ms)(uint32_t ms);
void (*gpio_write)(uint8_t pin, uint8_t val);
uint8_t (*gpio_read)(uint8_t pin);
// ...
} HW_Interface_t;
// 驱动层(通用代码,不需改动)
typedef struct {
HW_Interface_t *hw;
uint8_t state;
} Device_t;
void device_init(Device_t *dev, HW_Interface_t *hw) {
dev->hw = hw;
// 使用hw->xxx()调用硬件函数
}
// STM32平台实现
HW_Interface_t stm32_hw = {
.delay_ms = HAL_Delay,
.gpio_write = stm32_gpio_write,
.gpio_read = stm32_gpio_read
};
// Arduino平台实现
HW_Interface_t arduino_hw = {
.delay_ms = delay,
.gpio_write = digitalWrite,
.gpio_read = digitalRead
};
5. printf重定向会影响实时性吗?如何优化?
答案:
影响: printf是阻塞操作,一条消息可能耗时数ms,严重影响实时性。
优化方案:
- 关闭调试输出: Release版本不使用printf
- 使用DMA: UART发送不阻塞CPU
- 日志缓冲: 先写入缓冲区,后台任务慢慢发送
- ITM/SWO: 不占用串口,速度快
// 非阻塞日志系统
#define LOG_BUFFER_SIZE 1024
char log_buffer[LOG_BUFFER_SIZE];
uint16_t log_write_idx = 0;
void log_printf(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
int len = vsnprintf(&log_buffer[log_write_idx],
LOG_BUFFER_SIZE - log_write_idx,
fmt, args);
log_write_idx += len;
va_end(args);
}
// 后台任务发送
void log_task(void) {
while (1) {
if (log_write_idx > 0) {
HAL_UART_Transmit_DMA(&huart1, (uint8_t*)log_buffer, log_write_idx);
log_write_idx = 0;
}
vTaskDelay(100);
}
}