STM32F1 冬季作业学习文档
文档信息
- 适用平台: STM32F103 + FreeRTOS + HAL库
- 项目路径:
D:\01_Workspace\winter_homework - 更新日期: 2026-02-23
目录
一、项目总览
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 越亮。
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中断 方式接收串口数据,数据流如下:
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_count、storage_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 |
文档结束