串口通信

基础的串口交互

代码用于while循环中,代码需要挂起等待,有缓冲区,缓冲区有限,容易丢字节

//定义一个缓冲区
uint_8t recive_buff[16]={0};
while(1)
{
// 1: 串口编号 句柄  2: 数据  3: 数据长度 4.超时时间
//接收
HAL_UART_Receive(&huart1, receive_buff, 16, HAL_MAX_DELAY);
//判断是否有数据
if (strlen((char *)receive_buff) > 0)
{
    HAL_UART_Transmit(&huart1, receive_buff, 16, 1000);
    //清空为0
    memset(receive_buff, 0, 16);
}

基础的串口交互优化

利用空闲帧,支持任意串口数据接收回显,特殊函数->挂起等待->idle空闲帧模式->一个地址来存储实际字节长度

缓冲区大小不足,会造成单次接收数据丢失,常用的单次接收缓冲区是256字节

// 接收缓冲区(16字节,初始化为0)
uint8_t receive_buff[16] = {0};
// 实际接收的数据长度
uint16_t receive_len = 0;
// 串口空闲接收:接收数据直到总线空闲,最大16字节
HAL_UARTEx_ReceiveToIdle(&huart1, receive_buff, 16, &receive_len, HAL_MAX_DELAY);

// 判断:成功接收到数据
if(receive_len > 0)
{
    // 直接用实际接收长度发送,杜绝strlen越界风险
    HAL_UART_Transmit(&huart1, receive_buff, receive_len, 1000);
    
    // 清空缓冲区 + 重置长度,为下一次接收做准备
    memset(receive_buff, 0, sizeof(receive_buff));
    receive_len = 0;
}

串口交互中断

此函数是以空闲中断作为接收完成的标志,而不是接收长度,因此可以接收任意长度的数据
__HAL_UART_CLEAR_IDLEFLAG(&huart1)
清除空闲中断标志:防止初始化时误触发空闲中断,保证程序稳定

__HAL_UART_CLEAR_OREFLAG(&huart1) 清除串口溢出标志:如果接收速度过快,串口会产生溢出,不清除会导致接收卡死,必须加

/**
 * @brief 串口空闲中断回调函数
 * @param huart 串口句柄
 * @param Size 实际接收的数据长度
 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    // 判断是否为串口1
    if(huart->Instance == USART1)
    {
        // 保存实际接收长度
        usart_rec_len = Size;
        // 将数据原样回发
        HAL_UART_Transmit_IT(&huart1, usart_rec_buff, usart_rec_len);

        // ============= 清除标志 + 重新开启下一次接收 =============
        __HAL_UART_CLEAR_OREFLAG(&huart1);  // 清除溢出标志
        __HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除空闲标志
        HAL_UARTEx_ReceiveToIdle_IT(&huart1, usart_rec_buff, LEN_R);
    }
}

串口交互DMA

由于是不定长数据的接收,因此缓冲区大小可以根据实际需求调整,只能大不能小,否则可能会丢失数据

// 不定长数据接收完成回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1)
    {
        // 使用DMA将接收到的数据发送回去
        HAL_UART_Transmit_DMA(&huart1, rx_data, Size);
        // 重新启动接收,使用Ex函数,接收不定长数据
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_data, sizeof(rx_data));
        // 关闭DMA传输过半中断(HAL库默认开启,但我们只需要接收完成中断)
        __HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT);
    }
}

Flash的交互

尚硅谷

Flash的擦除

**Flash 的写入规则是 “先擦除,再写入”,核心原因是:Flash 存储单元只能从 1 变为 0,无法直接从 0 变回 1

擦除单位:通常按 页(Page) 擦除(也可执行整片擦除),擦除会清空整个页的所有数据。

检查页内字节是否为0xFF,如果是的话就擦除,计算该字节的起始地址->地址 - (地址 % 页大小)

/**
 * 智能擦除Flash:检查写入区域是否已有数据,有则擦除整个页
 */
static void Int_flash_erase(void)
{
    uint8_t need_erase = 0;      // 是否需要擦除:1要,0不要
    uint32_t PAGE_addr = 0;      // 需要擦除的页起始地址

    // 检查从写入起始地址开始的 usart_len 个字节,看是否有非0xFF的数据
    for (uint8_t i = 0; i < usart_len; i++)
    {
        // 读取Flash中对应位置的数据
        uint8_t data = *(volatile uint8_t*)(APP_START_ADDR + flash_write_offset + i);
        if (data != 0xFF)   // 只要有一个字节不是0xFF(说明已被写过)
        {
            need_erase = 1;  // 标记需要擦除
            // 计算出该字节所在的页起始地址(页对齐)
            PAGE_addr = (APP_START_ADDR + flash_write_offset + i) -
                        ((APP_START_ADDR + flash_write_offset + i) % FLASH_PAGE_SIZE);
            break;           // 找到一个就够了,不用继续查
        }
    }

    // 如果需要擦除,就擦除整个页
    if (need_erase)
    {
        FLASH_EraseInitTypeDef erase_init;      // 擦除配置结构体
        erase_init.TypeErase = FLASH_TYPEERASE_PAGES; // 按页擦除
        erase_init.PageAddress = APP_START_ADDR;      // 要擦除的页地址(这里固定从APP_START_ADDR开始,有问题)
        erase_init.NbPages = 1;                 // 擦除1页
        uint32_t page_error = 0;                // 记录出错页(未使用)
        HAL_FLASHEx_Erase(&erase_init, &page_error); // 执行擦除
    }
}

Flash的写入

写入前:必须优先执行解锁操作,解除写保护,否则擦写指令会被硬件拒绝。

写入完成后:重新执行上锁操作,恢复 Flash 保护状态,避免程序运行中被意外修改。

支持的单次写入位宽:仅支持 16 位(半字)、32 位(字)、64 位(双字) 写入不支持直接按单字节写入。

字节序:默认采用 小端序(Little-Endian)存储,即数据的低字节存放在低地址高字节存放在高地址

如果没有字节遗留的情况下直接按16位写入


// 处理没有遗留字节的情况,直接按16位写入
static void Int_flash_write_no_last(void)
{
    // 正好能够写入 => 不再有遗留的字节  0   6
    for (uint16_t i = 0; i < usart_len; i += 2)
    {
        uint32_t flash_addr = APP_START_ADDR + i + flash_write_offset;
        uint16_t data16;

        if (i + 1 < usart_len)
        {
            data16 = usart_rec_buff[i] | (usart_rec_buff[i + 1] << 8);
            HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_addr, data16);
        }
    }
}

处理有遗留字节的情况,使用上次遗留的字节和当前数据的第一个字节组成16位数据写入Flash


// 处理有遗留字节的情况,使用上次遗留的字节和当前数据的第一个字节组成16位数据写入Flash
static void Int_flash_write_with_last(void)
{
	for(uint16_t i=0;i<usart_len;i+=2)
{ 
     uint32_t flash_addr = APP_START_ADDR + flash_write_offset+i;
      uint16_t data16;
if(i==0) // 第一个16位数据,使用上次遗留的字节和当前数据的第一个字节组成
{
//拼接上一次字节
data16 =  last_byte |(usart_rec_buff[i] << 8) ; // 上次遗留字节在高位,当前第一个字节在低位
}
else
{
data16 = usart_rec_buff[i-1] | (usart_rec_buff[i] << 8); // 低字节在前,高字节在后
}
  
 HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_addr, data16);
}
}

奇偶长度+尾字节控制,总长度 = 当前接收长度 + 尾字节(0/1),
🔹 总长度 = 偶数
有遗留 (flag=1) → 拼接写入 → offset+=(len+1) → flag=0
无遗留 (flag=0) → 直接写入 → offset+=len → flag=0
🔹 总长度 = 奇数
有遗留 (flag=1) → 拼接写入 → 缓存最后 1 字节 → offset+=len → flag=1
无遗留 (flag=0) → 直接写入 → 缓存最后 1 字节 → offset+=(len-1) → flag=1

static void Int_flash_write_halfword(void)
{
    // 本次之后没剩余:总字节数为偶数(无遗留)
    if ((uart_rec_len + last_byte_flag) % 2 == 0)
    {
        if (last_byte_flag)
        {
            // 有剩余 数据长度是奇数 5 => 这次需要作为第一个字节写入
            Int_flash_write_with_last();
            // 2.4 记录偏移量
            flash_write_offset += uart_rec_len + 1;
        }
        else
        {
            // 无剩余 数据长度是偶数 => 不再有遗留的字节 0  6
            Int_flash_write_no_last();
            // 2.4 记录偏移量
            flash_write_offset += uart_rec_len;
        }
        last_byte_flag = 0; // 清除遗留标志
    }
    else // 本次之后有剩余:总字节数为奇数(遗留1字节)
    {
        if (last_byte_flag)
        {
            // 有剩余 数据长度是偶数
            Int_flash_write_with_last();
            // 修改最后剩下的字节
            last_byte = uart_rec_buff[uart_rec_len - 1];
            // 2.4 记录偏移量
            flash_write_offset += uart_rec_len;
        }
        else
        {
            // 上次没有遗留字节 这次会留下一个
            Int_flash_write_no_last();
            last_byte = uart_rec_buff[uart_rec_len - 1];
            // 2.4 记录偏移量
            flash_write_offset += uart_rec_len - 1;
        }
        last_byte_flag = 1; // 设置遗留标志
    }
}

回调函数:HAL_UARTEx_RxEventCallback (),接收数据 → 记录长度 → 解锁 Flash → 擦除 → 写入 → 锁 Flash → 清缓存 → 重启接收

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	if(huart->Instance==USART1)
	{
		usart_len=Size;
		sum+=usart_len;	

        HAL_FLASH_Unlock();

        Int_flash_erase();

        Int_flash_write_halfword();

             HAL_FLASH_Lock();

	memset(usart_rec_buff,0,BOOTLOADER_UART_BUFF);
	__HAL_UART_CLEAR_OREFLAG(&huart1);
	__HAL_UART_CLEAR_IDLEFLAG(&huart1);
	HAL_UARTEx_ReceiveToIdle_IT(&huart1,usart_rec_buff,BOOTLOADER_UART_BUFF);

}
}

Flash的初始化

在初始化内擦除十页,减少运行时间,防止丢失串口数据

void  int_bootloader(void)
{
     HAL_FLASH_Unlock();
     FLASH_EraseInitTypeDef erase_init;  // Flash擦除配置结构体
            erase_init.TypeErase = FLASH_TYPEERASE_PAGES;  // 擦除模式:按页擦除
            erase_init.PageAddress = APP_START_ADDR;           // 要擦除的页起始地址
            erase_init.NbPages = 10;                       // 只擦除10页
            uint32_t page_error = 0;                      // 擦除错误码
            HAL_FLASHEx_Erase(&erase_init, &page_error);  // 执行擦除 

              HAL_FLASH_Lock();

	__HAL_UART_CLEAR_OREFLAG(&huart1);
	__HAL_UART_CLEAR_IDLEFLAG(&huart1);
	HAL_UARTEx_ReceiveToIdle_IT(&huart1,usart_rec_buff,BOOTLOADER_UART_BUFF);
}

Flash的完整代码

#include  "int_bootloader.h"
uint8_t usart_rec_buff[BOOTLOADER_UART_BUFF]={0};
uint16_t  usart_len=0;
volatile int sum = 0;
uint32_t  flash_write_offset=0;
//末尾可能出现的单字节
uint8_t last_byte_flag = 0;
uint8_t last_byte = 0;
static void Int_flash_write_no_last(void);
static void Int_flash_write_with_last(void);
static void Int_flash_write_halfword(void);
static void Int_flash_erase(void)






void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	if(huart->Instance==USART1)
	{
		usart_len=Size;
		sum+=usart_len;	

        HAL_FLASH_Unlock();

        Int_flash_erase();

        Int_flash_write_halfword();

             HAL_FLASH_Lock();

	memset(usart_rec_buff,0,BOOTLOADER_UART_BUFF);
	__HAL_UART_CLEAR_OREFLAG(&huart1);
	__HAL_UART_CLEAR_IDLEFLAG(&huart1);
	HAL_UARTEx_ReceiveToIdle_IT(&huart1,usart_rec_buff,BOOTLOADER_UART_BUFF);

}
}



void  int_bootloader(void)
{
     HAL_FLASH_Unlock();
     FLASH_EraseInitTypeDef erase_init;  // Flash擦除配置结构体
            erase_init.TypeErase = FLASH_TYPEERASE_PAGES;  // 擦除模式:按页擦除
            erase_init.PageAddress = APP_START_ADDR;           // 要擦除的页起始地址
            erase_init.NbPages = 10;                       // 只擦除10页
            uint32_t page_error = 0;                      // 擦除错误码
            HAL_FLASHEx_Erase(&erase_init, &page_error);  // 执行擦除 

              HAL_FLASH_Lock();

	__HAL_UART_CLEAR_OREFLAG(&huart1);
	__HAL_UART_CLEAR_IDLEFLAG(&huart1);
	HAL_UARTEx_ReceiveToIdle_IT(&huart1,usart_rec_buff,BOOTLOADER_UART_BUFF);
}

int fputc(int ch, FILE *f)
{
  // 将字符发送到USART1
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
    return ch;
}

static void Int_flash_erase(void)
{

 uint8_t need_erase = 0;        // 擦除标志:1=需要擦除页,0=不需要
uint32_t PAGE_addr = 0;        // 待擦除的Flash页地址

// ===================== 2. 检测当前Flash页是否需要擦除 =====================
for(uint8_t i=0;i<usart_len;i++)
{
uint8_t data=*(volatile uint8_t*)(APP_START_ADDR+flash_write_offset+i);
if(data!=0xFF) // 只要有一个字节非0xFF,就需要擦除,直接退出循环
{
need_erase=1;  
PAGE_addr=(APP_START_ADDR+flash_write_offset+i)-((APP_START_ADDR+flash_write_offset+i)%FLASH_PAGE_SIZE);
	break;
}
}
  // ===================== 3. 执行Flash页擦除操作 =====================
if(need_erase)
{
   FLASH_EraseInitTypeDef erase_init;  // Flash擦除配置结构体
            erase_init.TypeErase = FLASH_TYPEERASE_PAGES;  // 擦除模式:按页擦除
            erase_init.PageAddress = APP_START_ADDR;           // 要擦除的页起始地址
            erase_init.NbPages = 1;                       // 只擦除1页
            uint32_t page_error = 0;    
            HAL_FLASHEx_Erase(&erase_init, &page_error);  // 执行擦除 
}	
}









// 处理没有遗留字节的情况,直接按16位写入
static void Int_flash_write_no_last(void)
{
    // 正好能够写入 => 不再有遗留的字节  0   6
    for (uint16_t i = 0; i < usart_len; i += 2)
    {
        uint32_t flash_addr = APP_START_ADDR + i + flash_write_offset;
        uint16_t data16;

        if (i + 1 < usart_len)
        {
            data16 = usart_rec_buff[i] | (usart_rec_buff[i + 1] << 8);
            HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_addr, data16);
        }
    }
}

// 处理有遗留字节的情况,使用上次遗留的字节和当前数据的第一个字节组成16位数据写入Flash
static void Int_flash_write_with_last(void)
{
	for(uint16_t i=0;i<usart_len;i+=2)
{ 
     uint32_t flash_addr = APP_START_ADDR + flash_write_offset+i;
      uint16_t data16;
if(i==0) // 第一个16位数据,使用上次遗留的字节和当前数据的第一个字节组成
{
//拼接上一次字节
data16 =  last_byte |(usart_rec_buff[i] << 8) ; // 上次遗留字节在高位,当前第一个字节在低位
}
else
{
data16 = usart_rec_buff[i-1] | (usart_rec_buff[i] << 8); // 低字节在前,高字节在后
}
  
 HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_addr, data16);
}
}



static void Int_flash_write_halfword(void)
{
    // 本次之后没剩余:总字节数为偶数(无遗留)
    if ((usart_len + last_byte_flag) % 2 == 0)
    {
        if (last_byte_flag)
        {
            // 有剩余 数据长度是奇数 5 => 这次需要作为第一个字节写入
            Int_flash_write_with_last();
            // 2.4 记录偏移量
            flash_write_offset += usart_len + 1;
        }
        else
        {
            // 无剩余 数据长度是偶数 => 不再有遗留的字节 0  6
            Int_flash_write_no_last();
            // 2.4 记录偏移量
            flash_write_offset += usart_len;
        }
        last_byte_flag = 0; // 清除遗留标志
    }
    else // 本次之后有剩余:总字节数为奇数(遗留1字节)
    {
        if (last_byte_flag)
        {
            // 有剩余 数据长度是偶数
            Int_flash_write_with_last();
            // 修改最后剩下的字节
            last_byte = usart_rec_buff[usart_len - 1];
            // 2.4 记录偏移量
            flash_write_offset += usart_len;
        }
        else
        {
            // 上次没有遗留字节 这次会留下一个
            Int_flash_write_no_last();
            last_byte = usart_rec_buff[usart_len - 1];
            // 2.4 记录偏移量
            flash_write_offset += usart_len - 1;
        }
        last_byte_flag = 1; // 设置遗留标志
    }
}
#ifndef __INT_BOOTLOADER_H__
#define __INT_BOOTLOADER_H__
#include "usart.h"
#include "string.h"
#include <stdio.h>
#define BOOTLOADER_UART_BUFF 512
#define APP_START_ADDR 0x08004000 //起始地址
#define APP_ERASE_PAGE_COUNT 10

void  int_bootloader(void);
void  int_bootloader_finish(void);
int fputc(int ch, FILE *f);

#endif 

总结

串口通信

轮询:简单但易丢数据。

空闲帧:HAL_UARTEx_ReceiveToIdle 支持不定长接收,需记录长度。

中断:回调中必须清除空闲标志和溢出标志,防止卡死。

DMA:配合空闲中断实现高效接收,回调中重启DMA并关闭半传输中断

Flash交互

规则:Flash只能1→0,擦除(页/整片)才能0→1;写前解锁,写后上锁。

擦除:动态检查非0xFF则擦页;或启动时预擦除10页,减少运行耗时。

半字写入:不支持单字节,用last_byte_flag和last_byte处理跨包拼接,根据总长度奇偶更新偏移。

流程:解锁→擦除(如需)→半字写入→上锁→清缓冲→重启接收

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐