跳转至

STM32F1 冬季作业学习文档

文档信息

  • 适用平台: STM32F103 + FreeRTOS + HAL库
  • 项目路径: D:\01_Workspace\winter_homework
  • 更新日期: 2026-02-23

目录

  1. 项目总览
  2. PWM呼吸灯模块
  3. 定时器计时模块
  4. 按键控制模块
  5. 串口通信模块
  6. SPI Flash存储模块
  7. OLED显示模块(含I2C)
  8. FreeRTOS多任务架构

一、项目总览

1.1 功能说明

本项目在 STM32F103 开发板上实现了以下功能:

功能 实现方式
呼吸灯 TIM3 PWM输出,表明程序正常运行
运行计时 FreeRTOS任务每秒递增计数器
串口存储控制 按KEY0开始存储,按KEY1停止存储
数据持久化 串口接收的数据写入SPI Flash
存储列表查询 按WKUP打印已存储消息的列表
数据查询 串口输入编号,打印对应数据内容
OLED显示 I2C接口,显示时间/消息数/容量/状态

1.2 系统初始化流程

程序入口在 main.c,调用 bsp_init() 完成所有外设初始化,然后启动 FreeRTOS 调度器。

/* 文件: User/Application/Src/main.c */

int main(void) {
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
    bsp_init();       /* 初始化所有外设 */
    freertos_start(); /* 启动FreeRTOS调度器 */
    return 0;
}
/* 文件: Drivers/Bsp/bsp.c */

void bsp_init(void) {
    HAL_Init();                    /* HAL库初始化 */
    system_clock_config();         /* 系统时钟:HSE 8MHz × PLL9 = 72MHz */
    delay_init(72);                /* 延时初始化 */
    usart1_init(115200);           /* 串口1,波特率115200 */
    led_init();                    /* LED初始化 */
    key_init();                    /* 按键初始化 */
    i2c1_init(400, 0, I2C_ADDRESSINGMODE_7BIT); /* I2C1,400kHz(供OLED使用) */
    spi1_init(SPI_MODE_MASTER, SPI_CLK_MODE0,
              SPI_DATASIZE_8BIT, SPI_FIRSTBIT_MSB); /* SPI1(供Flash使用) */
    norflash_init();               /* NOR Flash初始化 */
}

二、PWM呼吸灯模块

2.1 原理说明

呼吸灯通过改变 PWM 波的占空比来控制 LED 亮度: - 占空比从 0% 线性增大到 100%,LED 逐渐变亮 - 占空比从 100% 线性减小到 0%,LED 逐渐变暗 - 循环往复,形成"呼吸"效果

占空比 = 高电平时间 / 周期时间。占空比越大,LED 越亮。

PWM波形示意:
占空比10%: ▁▁▁▁▁▁▁▁▁█  (暗)
占空比50%: ▁▁▁▁▁█████  (中等亮度)
占空比90%: ▁█████████  (亮)

2.2 硬件配置

  • 定时器: TIM3,通道2
  • 引脚: PB5(复用推挽输出,部分重映射)
  • PWM频率: 72MHz ÷ (72) ÷ (500) = 2000Hz

2.3 PWM初始化代码

/* 文件: Drivers/Bsp/gtim/gtim.c */

TIM_HandleTypeDef g_timx_handle;

/* 通用定时器 TIM3 通道2 PWM输出初始化函数
 * arr: 自动重装载值(决定周期)
 * psc: 预分频值(决定计数频率)
 * PWM频率 = 72MHz / (psc+1) / (arr+1)
 */
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef timx_oc_pwm_chy;

    g_timx_handle.Instance = TIM3;          /* 使用TIM3 */
    g_timx_handle.Init.Prescaler = psc;     /* 预分频:72-1,计数频率=1MHz */
    g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 向上计数 */
    g_timx_handle.Init.Period = arr;        /* 自动重装:500-1 */
    HAL_TIM_PWM_Init(&g_timx_handle);

    timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;       /* PWM模式1 */
    timx_oc_pwm_chy.Pulse = arr / 2;                 /* 初始占空比50% */
    timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH; /* 高电平有效 */

    HAL_TIM_PWM_ConfigChannel(&g_timx_handle, &timx_oc_pwm_chy, TIM_CHANNEL_2);
    HAL_TIM_PWM_Start(&g_timx_handle, TIM_CHANNEL_2); /* 启动PWM */
}

/* MSP回调:配置TIM3对应的GPIO引脚 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3) {
        GPIO_InitTypeDef gpio_initure = {0};

        __HAL_RCC_GPIOB_CLK_ENABLE();   /* 使能GPIOB时钟 */
        __HAL_RCC_TIM3_CLK_ENABLE();    /* 使能TIM3时钟 */

        gpio_initure.Pin = GPIO_PIN_5;
        gpio_initure.Mode = GPIO_MODE_AF_PP;    /* 复用推挽输出 */
        gpio_initure.Pull = GPIO_PULLUP;
        gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOB, &gpio_initure);

        /* TIM3部分重映射:CH2从PA7重映射到PB5 */
        __HAL_RCC_AFIO_CLK_ENABLE();
        __HAL_AFIO_REMAP_TIM3_PARTIAL();
    }
}

2.4 呼吸灯任务代码

/* 文件: User/Application/Src/rtos_tasks.c */

void task_pwm(void *pvParameters)
{
    uint16_t duty = 0;   /* 当前占空比(0~500) */
    int8_t dir = 1;      /* 方向:+1增大,-1减小 */

    const uint16_t PWM_MAX = 500;  /* 最大占空比值(对应100%) */
    const uint16_t PWM_STEP = 5;   /* 每步变化量 */

    /* 初始化PWM:ARR=499, PSC=71,周期≈2000Hz */
    gtim_timx_pwm_chy_init(500 - 1, 72 - 1);

    while (1) {
        duty += dir * PWM_STEP;    /* 按步长改变占空比 */

        if (duty >= PWM_MAX) {
            duty = PWM_MAX;
            dir = -1;   /* 到达最大值,反向减小 */
        } else if (duty == 0) {
            duty = 0;
            dir = 1;    /* 到达0,反向增大 */
        }

        /* 写入新的比较值,即时改变占空比 */
        __HAL_TIM_SET_COMPARE(&g_timx_handle, TIM_CHANNEL_2, duty);

        vTaskDelay(20);  /* 每20ms更新一次,完整一次呼吸约4秒 */
    }
}

关键点说明: - __HAL_TIM_SET_COMPARE() 直接修改定时器的比较寄存器(CCR),无需重新初始化 - vTaskDelay(20) 使任务每20ms运行一次,让出CPU给其他任务 - 占空比从0到500共100步,100步×20ms = 2秒完成一次变亮/变暗,完整呼吸周期约4秒


三、定时器计时模块

3.1 实现思路

本项目没有使用硬件定时器中断来计时,而是利用 FreeRTOS 的任务延时来实现秒级计时:创建一个专用任务,每次执行时将计数器加1,然后延时1000ms,如此循环即可实现秒级计时。

3.2 计时任务代码

/* 文件: User/Application/Src/rtos_tasks.c */

/* 全局运行时间(秒),供其他任务读取 */
static volatile uint32_t g_run_time_seconds = 0;

/* task2:系统计时任务,每秒递增一次计数器 */
void task2(void *pvParameters)
{
    while (1) {
        g_run_time_seconds++;    /* 运行时间+1秒 */
        vTaskDelay(1000);        /* 等待1000个tick(即1秒) */
    }
}

关键点说明: - g_run_time_seconds 声明为 volatile,防止编译器优化,确保每次都从内存中读取最新值 - vTaskDelay(1000) 在 FreeRTOS 中默认 1 tick = 1ms,所以 1000 tick = 1秒 - 其他任务通过读取 g_run_time_seconds 获取程序已运行的秒数

3.3 时间格式化(OLED显示用)

/* 文件: User/Application/Src/rtos_tasks.c(Oled_task内) */

uint32_t hours, mins, secs;

/* 将总秒数转换为 时:分:秒 格式 */
hours = g_run_time_seconds / 3600;
mins  = (g_run_time_seconds % 3600) / 60;
secs  = g_run_time_seconds % 60;

sprintf(buf, "Time:%02u:%02u:%02u", hours, mins, secs);

四、按键控制模块

4.1 硬件说明

按键 引脚 有效电平 内部上下拉 功能
KEY0 PC5 低电平(按下接地) 上拉 开始存储
KEY1 PA15 低电平(按下接地) 上拉 停止存储
WKUP PA0 高电平(按下接VCC) 下拉 显示存储列表

4.2 按键初始化代码

/* 文件: Drivers/Bsp/key/key.c */

void key_init(void) {
    GPIO_InitTypeDef gpio_initure = {0};

    /* KEY0:PC5,上拉输入(按下为低电平) */
    CSP_GPIO_CLK_ENABLE(KEY0_GPIO_PORT);
    gpio_initure.Pin = KEY0_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_INPUT;
    gpio_initure.Pull = GPIO_PULLUP;
    gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(CSP_GPIO_PORT(KEY0_GPIO_PORT), &gpio_initure);

    /* KEY1:PA15,上拉输入(按下为低电平) */
    CSP_GPIO_CLK_ENABLE(KEY1_GPIO_PORT);
    gpio_initure.Pin = KEY1_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_INPUT;
    gpio_initure.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(CSP_GPIO_PORT(KEY1_GPIO_PORT), &gpio_initure);

    /* WKUP:PA0,下拉输入(按下为高电平) */
    CSP_GPIO_CLK_ENABLE(WKUP_GPIO_PORT);
    gpio_initure.Pin = WKUP_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_INPUT;
    gpio_initure.Pull = GPIO_PULLDOWN;
    HAL_GPIO_Init(CSP_GPIO_PORT(WKUP_GPIO_PORT), &gpio_initure);
}

4.3 按键扫描函数

/* 文件: Drivers/Bsp/key/key.c */

/* 读取各按键引脚电平的宏定义 */
#define KEY0  HAL_GPIO_ReadPin(CSP_GPIO_PORT(KEY0_GPIO_PORT), KEY0_GPIO_PIN)
#define KEY1  HAL_GPIO_ReadPin(CSP_GPIO_PORT(KEY1_GPIO_PORT), KEY1_GPIO_PIN)
#define WK_UP HAL_GPIO_ReadPin(CSP_GPIO_PORT(WKUP_GPIO_PORT), WKUP_GPIO_PIN)

/*
 * 按键扫描函数(轮询 + 软件消抖)
 * scan_continous = 0:不支持连按(普通模式)
 * scan_continous = 1:支持连按(连续按住持续触发)
 * 优先级:KEY0 > KEY1 > WKUP
 */
key_press_t key_scan(uint8_t scan_continous)
{
    static uint8_t key_up = 1; /* 按键释放标志,静态变量保持状态 */

    if (scan_continous == 1) {
        key_up = 1; /* 连按模式:每次进入都视为按键已释放 */
    }

    /* 检测到任意按键按下 */
    if (key_up && (KEY0 == 0 || KEY1 == 0 || WK_UP == 1)) {
        delay_ms(10);   /* 消抖延时10ms */
        key_up = 0;     /* 标记按键已按下,防止重复触发 */

        if (KEY0 == 0) {
            return KEY0_PRESS;
        } else if (KEY1 == 0) {
            return KEY1_PRESS;
        } else if (WK_UP == 1) {
            return WKUP_PRESS;
        }
    } else if (KEY0 == 1 && KEY1 == 1 && WK_UP == 0) {
        key_up = 1;     /* 所有按键释放,重新允许检测 */
    }

    return KEY_NO_PRESS;
}

消抖原理: - 按键按下瞬间,由于机械抖动会产生几毫秒的信号抖动 - 检测到电平变化后,延时10ms跳过抖动期,再次读取确认 - key_up 静态变量确保一次按下只触发一次,松手后才能再次触发

4.4 按键处理任务(task3)

/* 文件: User/Application/Src/rtos_tasks.c */

static volatile uint8_t storage_enabled = 0; /* 存储使能标志 */

void task3(void *pvParameters)
{
    key_press_t key = KEY_NO_PRESS;

    while (1) {
        key = key_scan(0);  /* 普通模式扫描按键 */

        switch (key) {
            case KEY0_PRESS: {
                storage_enabled = 1;  /* 允许存储 */
                printf("[KEY0] Storage Started\r\n");
                printf("       Storage mode enabled, data will be saved to buffer\r\n");
            } break;

            case KEY1_PRESS: {
                storage_enabled = 0;  /* 禁止存储 */
                printf("[KEY1] Storage Stopped\r\n");
                printf("       Total saved: %d items\r\n", storage_count);
            } break;

            case WKUP_PRESS: {
                /* 打印所有已存储消息的列表 */
                storage_item_t item;
                printf("[WKUP] Storage List:\r\n");
                printf("       No. | Time(sec) | Len | Data\r\n");
                printf("       ---------------------------\r\n");

                for (int i = 0; i < storage_count && i < MAX_ITEMS; i++) {
                    /* 从Flash读取第i+1条记录 */
                    norflash_read((uint8_t *)&item, MSG_ADDR(i + 1), sizeof(item));
                    if (item.valid) {
                        /* 过滤掉换行符,提取可打印内容 */
                        char data_preview[21];
                        int j, k;
                        for (j = 0, k = 0; j < item.length && k < 20; j++) {
                            if (item.data[j] != '\r' && item.data[j] != '\n') {
                                data_preview[k++] = item.data[j];
                            }
                        }
                        data_preview[k] = '\0';

                        printf("       %2d   | %8u | %4d | %s\r\n",
                               i + 1,
                               item.timestamp,
                               item.length,
                               data_preview);
                    }
                }
                printf("       ---------------------------\r\n");
                printf("       Enter number (1-%d) to view details\r\n", storage_count);
            } break;

            default: break;
        }

        vTaskDelay(10); /* 每10ms扫描一次按键 */
    }
}

五、串口通信模块

5.1 架构说明

本项目使用 DMA + IDLE中断 方式接收串口数据,数据流如下:

串口数据 → USART1硬件接收 → DMA自动搬运到环形FIFO缓冲
                            task_uart_receive 每10ms轮询读取
                            判断存储状态 → 写入Flash 或 查询数据

DMA(直接内存访问)的优势:CPU 不参与数据搬运,数据直接从 USART 寄存器传输到内存缓冲区,CPU 可以处理其他任务,效率更高。

5.2 串口初始化

CSP层(芯片支持包)已封装好初始化接口,调用一行即可:

/* 文件: Drivers/Bsp/bsp.c */

usart1_init(115200);  /* 初始化串口1,波特率115200 */
                      /* CSP层内部配置:PA9(TX)、PA10(RX)、DMA循环接收、IDLE中断 */

5.3 数据存储结构

每条串口消息保存为一个结构体,存储在 Flash 中:

/* 文件: User/Application/Src/rtos_tasks.c */

#define MAX_DATA_LENGTH 20    /* 每条数据最大20字节 */
#define MAX_ITEMS (2 * 128)   /* 最多256条,占用Flash的2个扇区 */

/* 单条消息存储结构体,共26字节 */
typedef struct {
    uint32_t timestamp;          /* 4字节:保存时的运行时间(秒) */
    uint8_t  data[MAX_DATA_LENGTH]; /* 20字节:实际数据内容 */
    uint8_t  length;             /* 1字节:有效数据长度 */
    uint8_t  valid;              /* 1字节:有效标志(1=有效,0xFF=空) */
} storage_item_t;

/* 计算第n条消息在Flash中的存储地址(n从1开始) */
#define MSG_ADDR(n) (0x1000 + ((n)-1) * 32)
/*
 * 地址0x1000 = Flash第1扇区起始(第0扇区留给其他用途)
 * 每条消息占32字节(结构体实际26字节,对齐到32字节)
 */

5.4 串口接收与处理任务

/* 文件: User/Application/Src/rtos_tasks.c */

void task_uart_receive(void *pvParameters)
{
    uint8_t rx_buf[UART_RX_BUF_SIZE];  /* 接收缓冲区 */
    uint32_t rx_len;
    storage_item_t item;

    printf("[UART_RX_TASK] Task created (Polling Mode)\r\n");
    printf("[UART_RX_TASK] KEY0:Start, KEY1:Stop, WKUP:List\r\n");

    /* 上电后扫描Flash,恢复已存储的消息数量 */
    if (!flash_initialized) {
        storage_item_t temp_item;
        for (storage_count = 0; storage_count < MAX_ITEMS; storage_count++) {
            norflash_read((uint8_t *)&temp_item, MSG_ADDR(storage_count + 1), sizeof(temp_item));
            /* 判断该位置是否有有效数据 */
            if (temp_item.valid != 1 || temp_item.length > MAX_DATA_LENGTH) {
                break;  /* 找到无效数据,说明后续没有更多数据了 */
            }
        }
        printf("[Flash] Restored %d items from Flash\r\n", storage_count);
        flash_initialized = 1;
    }

    while (1) {
        vTaskDelay(POLL_INTERVAL_MS);  /* 每10ms轮询一次 */

        /* 从DMA环形缓冲区读取数据 */
        rx_len = uart_dmarx_read(&usart1_handle, rx_buf, UART_RX_BUF_SIZE - 1);

        if (rx_len > 0) {
            rx_buf[rx_len] = '\0';

            /* 去除末尾的换行符 */
            while (rx_len > 0 && (rx_buf[rx_len-1] == '\r' || rx_buf[rx_len-1] == '\n')) {
                rx_len--;
                rx_buf[rx_len] = '\0';
            }

            /* 判断:若未在存储模式,且输入是纯数字 → 查询对应编号的数据 */
            if (!storage_enabled && is_number_string((char *)rx_buf)) {
                int index = string_to_int((char *)rx_buf);
                if (index > 0 && index <= storage_count) {
                    norflash_read((uint8_t *)&item, MSG_ADDR(index), sizeof(item));
                    if (item.valid) {
                        /* 提取并打印数据内容 */
                        char data_str[21];
                        int j, k;
                        for (j = 0, k = 0; j < item.length && k < 20; j++) {
                            if (item.data[j] != '\r' && item.data[j] != '\n') {
                                data_str[k++] = item.data[j];
                            }
                        }
                        data_str[k] = '\0';
                        printf("[Query Result] Index: %d\r\n", index);
                        printf("          Time: %u sec\r\n", item.timestamp);
                        printf("          Length: %d\r\n", item.length);
                        printf("          Data: %s\r\n", data_str);
                    }
                }
            } else {
                /* 正常接收数据 */
                printf("[UART_RX] Received: %s (len: %u)\r\n", rx_buf, rx_len);

                if (storage_enabled) {
                    /* 存储模式开启:将数据写入Flash */
                    if (storage_count < MAX_ITEMS) {
                        /* 第一次写入时,先检查并擦除目标扇区 */
                        if (storage_count == 0) {
                            uint8_t check_byte;
                            norflash_read(&check_byte, MSG_ADDR(1), 1);
                            if (check_byte != 0xFF) {
                                printf("[Flash] Erasing sector 1 for first write...\r\n");
                                norflash_erase_sector(0x1000 / 4096);
                            }
                        }

                        /* 填充存储结构体并写入Flash */
                        item.timestamp = g_run_time_seconds;
                        item.length = (rx_len > MAX_DATA_LENGTH) ? MAX_DATA_LENGTH : (uint8_t)rx_len;
                        memcpy(item.data, rx_buf, item.length);
                        item.valid = 1;

                        norflash_write((uint8_t *)&item, MSG_ADDR(storage_count + 1), sizeof(item));
                        storage_count++;
                        printf("[Storage] Saved at position %d\r\n", storage_count);
                    } else {
                        printf("[Storage] Buffer full, cannot save more data\r\n");
                    }
                }
            }
        }
    }
}

六、SPI Flash存储模块

6.1 Flash基本概念

SPI NOR Flash(W25Q64)特性

特性 参数
容量 64Mbit = 8MB
通信接口 SPI(PA5-SCK, PA6-MISO, PA7-MOSI, CS片选)
最小擦除单位 扇区(Sector)= 4KB
写入单位 页(Page)= 256字节
擦除特点 写入前必须擦除,擦除后全为0xFF

重要特性:Flash 只能将位从 1 写为 0,不能将 0 改回 1。要写入新数据,必须先"擦除"(将整个扇区恢复为0xFF),再写入。

6.2 硬件连接

STM32F103                W25Q64 (SPI NOR Flash)
┌─────────────┐          ┌─────────────┐
│  PA5 (SCK)  │─────────→│  CLK        │  时钟线
│  PA6 (MISO) │←─────────│  DO         │  数据输出(主机接收)
│  PA7 (MOSI) │─────────→│  DI         │  数据输入(主机发送)
│  CS(片选) │─────────→│  /CS        │  片选(低电平选中)
└─────────────┘          └─────────────┘

6.3 Flash初始化代码

/* 文件: Drivers/Bsp/norflash/norflash.c */

uint16_t g_norflash_type = W25Q64; /* 默认型号 */

void norflash_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;

    /* 配置片选引脚(CS)为推挽输出 */
    NORFLASH_CS_GPIO_CLK_ENABLE();
    gpio_init_struct.Pin = NORFLASH_CS_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(NORFLASH_CS_GPIO_PORT, &gpio_init_struct);

    NORFLASH_CS(1);  /* 取消片选,默认不选中 */

    /* 读取芯片ID,识别Flash型号 */
    g_norflash_type = norflash_read_id();
}

6.4 核心操作:读数据

/* 文件: Drivers/Bsp/norflash/norflash.c */

/*
 * 从Flash指定地址读取数据
 * pbuf:    接收数据的缓冲区指针
 * addr:    起始读取地址(24位/32位地址)
 * datalen: 要读取的字节数
 */
void norflash_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;

    NORFLASH_CS(0);                                        /* 拉低CS,选中Flash */
    spi_rw_one_byte(&spi1_handle, FLASH_ReadData);         /* 发送读命令0x03 */
    norflash_send_address(addr);                           /* 发送24位地址 */

    for (i = 0; i < datalen; i++) {
        pbuf[i] = spi_rw_one_byte(&spi1_handle, 0xFF);    /* 发0xFF,同时读回数据 */
    }

    NORFLASH_CS(1);                                        /* 拉高CS,释放Flash */
}

6.5 核心操作:写数据(带自动擦除)

/* 文件: Drivers/Bsp/norflash/norflash.c */

/*
 * 写入Flash(完整功能版,自动处理跨扇区和擦除)
 * 写入步骤:
 *   1. 读取目标扇区的全部4KB到内存缓冲区
 *   2. 检查目标区域是否已有非0xFF数据
 *   3. 如需擦除,先擦除整个扇区,再合并新数据,整扇区写回
 *   4. 如目标区域全为0xFF,直接写入(无需擦除)
 */
void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint32_t secpos;     /* 扇区号 */
    uint16_t secoff;     /* 在扇区内的偏移 */
    uint16_t secremain;  /* 扇区内剩余可写字节数 */

    secpos = addr / 4096;         /* 计算扇区号 */
    secoff = addr % 4096;         /* 扇区内偏移 */
    secremain = 4096 - secoff;    /* 本扇区剩余空间 */

    if (datalen <= secremain) {
        secremain = datalen;
    }

    while (1) {
        /* 将整个扇区读入内存缓冲区 */
        norflash_read(g_norflash_buf, secpos * 4096, 4096);

        /* 检查目标区域是否需要擦除(有非0xFF数据) */
        uint16_t i;
        for (i = 0; i < secremain; i++) {
            if (g_norflash_buf[secoff + i] != 0xFF) break;
        }

        if (i < secremain) {
            /* 需要擦除:擦除扇区后合并数据重新写入 */
            norflash_erase_sector(secpos);
            for (i = 0; i < secremain; i++) {
                g_norflash_buf[secoff + i] = pbuf[i]; /* 合并新数据 */
            }
            norflash_write_nocheck(g_norflash_buf, secpos * 4096, 4096);
        } else {
            /* 无需擦除:直接写入 */
            norflash_write_nocheck(pbuf, addr, secremain);
        }

        if (datalen == secremain) break; /* 写完了 */

        /* 继续写下一个扇区 */
        secpos++;
        secoff = 0;
        pbuf += secremain;
        addr += secremain;
        datalen -= secremain;
        secremain = (datalen > 4096) ? 4096 : datalen;
    }
}

6.6 核心操作:扇区擦除

/* 文件: Drivers/Bsp/norflash/norflash.c */

/*
 * 擦除一个扇区(4KB)
 * saddr: 扇区号(不是字节地址!传入n,内部会乘以4096)
 * 擦除后该扇区全部变为0xFF
 */
void norflash_erase_sector(uint32_t saddr)
{
    saddr *= 4096;                /* 将扇区号转换为字节地址 */
    norflash_write_enable();      /* 写使能(擦除前必须先写使能) */
    norflash_wait_busy();         /* 等待Flash空闲 */

    NORFLASH_CS(0);
    spi_rw_one_byte(&spi1_handle, FLASH_SectorErase);  /* 发送扇区擦除命令0x20 */
    norflash_send_address(saddr);                       /* 发送地址 */
    NORFLASH_CS(1);

    norflash_wait_busy();  /* 等待擦除完成(约150ms) */
}

/* 等待Flash完成当前操作(轮询BUSY位) */
static void norflash_wait_busy(void)
{
    while ((norflash_read_sr(1) & 0x01) == 0x01);  /* 状态寄存器1的bit0为BUSY位 */
}

6.7 地址布局设计

Flash地址空间:
0x000000 ─── Sector 0 (4KB) ─── 保留(系统数据等)
0x001000 ─── Sector 1 (4KB) ─── 消息存储区起始
             第1条消息: 0x001000 ~ 0x00101F (32字节)
             第2条消息: 0x001020 ~ 0x00103F (32字节)
             ...
             第128条消息: 0x001FE0 ~ 0x001FFF
0x002000 ─── Sector 2 (4KB) ─── 消息存储区(续)
             第129条 ~ 第256条消息

七、OLED显示模块(含I2C)

7.1 I2C协议简介

I2C(IIC)是一种两线式串行通信协议: - SDA(串行数据线):传输数据 - SCL(串行时钟线):提供时钟

通信特点: - 支持多主机多从机,每个设备有唯一地址 - 本项目 OLED 的 I2C 地址为 0x78(7位地址 0x3C 左移1位) - 通信速率:400kHz(快速模式)

7.2 硬件配置

信号 STM32引脚 说明
SCL PB6 I2C1时钟线
SDA PB7 I2C1数据线
OLED地址 0x78 7位地址0x3C,写地址0x78

7.3 OLED底层写入

/* 文件: Drivers/Bsp/oled/oled_hal.c */

/* 向OLED写入命令字节(控制字节=0x00) */
void OLED_WR_CMD(uint8_t cmd)
{
    /* 参数说明:
     * 0x78: OLED的I2C写地址
     * 0x00: 控制字节(表示后续是命令)
     * &cmd: 命令数据
     * 1: 发送1字节
     */
    HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 0x100);
}

/* 向OLED写入显示数据(控制字节=0x40) */
void OLED_WR_DATA(uint8_t data)
{
    HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, &data, 1, 0x100);
}

7.4 OLED初始化

/* 文件: Drivers/Bsp/oled/oled_hal.c */

/* OLED初始化命令序列(SSD1306控制芯片) */
uint8_t CMD_Data[] = {
    0xAE,       /* 关显示 */
    0xD5, 0x80, /* 设置时钟分频 */
    0xA8, 0x3F, /* 设置多路复用率(64行) */
    0xD3, 0x00, /* 设置显示偏移 */
    0x40,       /* 设置起始行 */
    0xA1,       /* 列地址映射(左右翻转) */
    0xC8,       /* 行扫描方向(上下翻转) */
    0xDA, 0x12, /* 设置COM引脚配置 */
    0x81, 0xCF, /* 设置对比度 */
    0xD9, 0xF1, /* 设置预充电周期 */
    0xDB, 0x40, /* 设置VCOMH */
    0xA4,       /* 正常显示(非全亮) */
    0xA6,       /* 正显(非反显) */
    0x8D, 0x14, /* 开启电荷泵 */
    0xAF        /* 开显示 */
};

void OLED_Init(void)
{
    HAL_Delay(200); /* 上电等待稳定 */

    for (uint8_t i = 0; i < 23; i++) {
        OLED_WR_CMD(CMD_Data[i]); /* 逐条发送初始化命令 */
    }
}

7.5 OLED显示函数说明

本项目使用了以下OLED显示函数(已封装好,直接调用):

/* 文件: Drivers/Bsp/oled/oled_hal.h(函数声明) */

void OLED_Init(void);    /* 初始化 */
void OLED_Clear(void);   /* 清屏(全黑) */

/* 显示字符串
 * x: 列坐标(0~127)
 * y: 行号(0, 2, 4, 6 对应4行,每行16像素)
 * chr: 字符串指针
 * Char_Size: 字体大小(12或16)
 * Color_Turn: 反色(0正常,1反色)
 */
void OLED_ShowString(uint8_t x, uint8_t y, char *chr, uint8_t Char_Size, uint8_t Color_Turn);

/* 显示数字 */
void OLED_ShowNum(uint8_t x, uint8_t y, unsigned int num, uint8_t len, uint8_t size2, uint8_t Color_Turn);

7.6 OLED显示任务

/* 文件: User/Application/Src/rtos_tasks.c */

void Oled_task(void *pvParameters)
{
    OLED_Init();   /* 初始化OLED */
    OLED_Clear();  /* 清屏 */

    char buf[32];
    uint32_t hours, mins, secs;

    while (1) {
        /* 第1行(y=0):显示程序运行时间 */
        hours = g_run_time_seconds / 3600;
        mins  = (g_run_time_seconds % 3600) / 60;
        secs  = g_run_time_seconds % 60;
        sprintf(buf, "Time:%02u:%02u:%02u", hours, mins, secs);
        OLED_ShowString(0, 0, buf, 12, 0);

        /* 第2行(y=2):显示Flash使用百分比 */
        uint32_t use_x100 = (storage_count * 10000) / MAX_ITEMS;
        sprintf(buf, "Use:%u.%02u%%", use_x100 / 100, use_x100 % 100);
        OLED_ShowString(0, 2, buf, 12, 0);

        /* 第3行(y=4):显示已存储消息数量 */
        sprintf(buf, "Msg:%d/%d", storage_count, MAX_ITEMS);
        OLED_ShowString(0, 4, buf, 12, 0);

        /* 第4行(y=6):显示当前状态 */
        const char *state = storage_enabled ? "STORING" : "IDLE   ";
        sprintf(buf, "State:%s", state);
        OLED_ShowString(0, 6, buf, 12, 0);

        vTaskDelay(100); /* 每100ms刷新一次显示 */
    }
}

OLED显示布局

┌────────────────────────┐
│ Time:00:05:23          │  第1行:运行时间
│ Use:2.34%              │  第2行:Flash使用率
│ Msg:6/256              │  第3行:消息数量
│ State:IDLE             │  第4行:当前状态
└────────────────────────┘


八、FreeRTOS多任务架构

8.1 为什么使用RTOS?

单纯的裸机(无RTOS)程序只能串行执行:要么做按键检测,要么刷新OLED,要么接收串口数据,无法同时处理多件事。

FreeRTOS 通过时间片调度模拟并发:多个任务轮流占用CPU,每个任务有自己的独立栈空间,看起来像是"同时运行"。

8.2 任务列表

任务名 优先级 栈大小 功能
start_task 2 128 启动任务,创建其他任务后自删除
task2 1 128 系统计时(每秒+1)
task3 2 128 按键扫描与处理
task_pwm 2 128 PWM呼吸灯控制
Oled_task 2 256 OLED显示刷新
task_uart_receive 3 256 串口数据接收与存储

8.3 任务启动代码

/* 文件: User/Application/Src/rtos_tasks.c */

/* FreeRTOS入口:创建起始任务,然后启动调度器 */
void freertos_start(void) {
    xTaskCreate(start_task, "start_task", 128, NULL, 2, &start_task_handle);
    vTaskStartScheduler();  /* 启动调度器,此行之后不再返回 */
}

/* 起始任务:负责创建所有其他任务 */
void start_task(void *pvParameters) {
    taskENTER_CRITICAL();  /* 进入临界区,防止任务创建时被打断 */

    xTaskCreate(task2,             "task2",    128, NULL, 1, &task2_handle);
    xTaskCreate(task3,             "task3",    128, NULL, 2, &task3_handle);
    xTaskCreate(task_pwm,          "task_pwm", 128, NULL, 2, &task_pwm_handle);
    xTaskCreate(Oled_task,         "oled_task",256, NULL, 2, &oled_task_handle);
    xTaskCreate(task_uart_receive, "uart_rx",  256, NULL, 3, &uart_receive_task_handle);

    vTaskDelete(start_task_handle);  /* 删除自身,节省内存 */
    taskEXIT_CRITICAL();
}

xTaskCreate 参数说明

xTaskCreate(
    task2,          /* 任务函数指针 */
    "task2",        /* 任务名称(调试用) */
    128,            /* 栈大小(单位:字,1字=4字节,即512字节) */
    NULL,           /* 传递给任务的参数 */
    1,              /* 优先级(数字越大优先级越高) */
    &task2_handle   /* 任务句柄,用于后续操作该任务 */
);

8.4 任务间共享数据保护

多个任务同时访问共享变量(如 storage_countstorage_enabled)时,需防止数据竞争:

/* 方法1:声明为 volatile,防止编译器缓存旧值 */
static volatile uint16_t storage_count = 0;
static volatile uint8_t storage_enabled = 0;

/* 方法2:临界区保护(关中断,防止任务切换)*/
taskENTER_CRITICAL();
storage_count++;           /* 在临界区内安全修改共享数据 */
taskEXIT_CRITICAL();

附录:各模块文件对照

功能模块 主要文件
主程序入口 User/Application/Src/main.c
所有任务实现 User/Application/Src/rtos_tasks.c
外设总初始化 Drivers/Bsp/bsp.c
PWM驱动 Drivers/Bsp/gtim/gtim.c
按键驱动 Drivers/Bsp/key/key.c
SPI Flash驱动 Drivers/Bsp/norflash/norflash.c
OLED显示驱动 Drivers/Bsp/oled/oled_hal.c
串口驱动(CSP层) Drivers/CSP/UART_STM32F1xx.c
I2C驱动(CSP层) Drivers/CSP/I2C_STM32F1xx.c
SPI驱动(CSP层) Drivers/CSP/SPI_STM32F1xx.c

文档结束

评论