🧰 二、工程框架搭建(续)
------ 系统初始化 + 超简单任务调度器
💡 想让单片机"听话"?先让它正确启动 ,再给它一个智能管家!
本章将带你:
✅ 修复并理解
system_init()的真实作用✅ 从零实现一个超轻量级任务调度器 (像手机闹钟一样提醒你做事!)
✅ 学会用结构体组织复杂数据(告别一堆零散变量!)
小白也能写出专业级嵌入式代码!
🔌 一、系统初始化函数 ------ 别被"假操作"骗了!
你看到的这段代码:
c
void system_init(void) {
GPIOC->ODR = 0x00ff; // 清除高8位?
GPIOC->ODR = ~(0x00 << 8); // 设高8位为高?
GPIOD->BSRR = 0x01 << 2; // 置位 PD2
GPIOD->BRR = 0x01 << 2; // 复位 PD2
}
⚠️ 其实存在严重问题!
❌ 问题分析:
| 行 | 问题 | 正确做法 |
|---|---|---|
GPIOC->ODR = 0x00ff |
直接写 ODR 会覆盖整个寄存器!低8位也被强制设为 0xFF,可能误关其他引脚 | 应使用 BSRR/BRR 或 先配置模式再操作 |
~(0x00 << 8) |
0x00 << 8 还是 0,~0 = 0xFFFFFFFF → 全部拉高!不是只设高8位 |
想设高8位为1:应写 0xFF00 |
| PD2 置位后立刻复位 | 速度太快,肉眼/示波器都看不到变化,毫无意义 | 若用于复位外设,需加延时;否则可删除 |
✅ 正确的系统初始化应该做什么?
真正的 system_init() 应包含:
- 时钟使能(RCC)
- GPIO 模式配置(输入/输出/复用)
- 外设初始化(UART、ADC 等)
🌟 示例:正确初始化 PC8~PC15 为推挽输出
c
#include "stm32f1xx.h"
void system_init(void) {
// 1. 使能 GPIOC 和 GPIOD 时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN | RCC_APB2ENR_IOPDEN;
// 2. 配置 PC8~PC15 为推挽输出,50MHz
GPIOC->CRH = 0x33333333; // 高8位:每4位控制1个引脚,0b0011 = 推挽输出 50MHz
// 3. 初始状态:全部拉低
GPIOC->ODR &= ~0xFF00; // 清除高8位(PC8~PC15)
// 4. (可选)PD2 作为普通输出
GPIOD->CRL &= ~(0xF << (2*4)); // 清除 PD2 配置
GPIOD->CRL |= (0x3 << (2*4)); // 推挽输出 50MHz
GPIOD->ODR &= ~(1 << 2); // 初始拉低
}
✅ 关键原则:
- 先配模式,再设电平
- 操作寄存器前,先开时钟!
- 避免直接写 ODR,除非你确定所有引脚状态
⏰ 二、任务调度器 ------ 给单片机请个"智能管家"
🎯 什么是任务调度器?
想象你是个学生,每天要:
- 7:00 吃早餐
- 10:00 做作业
- 16:00 锻炼
- 22:00 睡觉
但你不想自己记时间------于是请了个管家,他每秒看一次表,到点就提醒你!
在嵌入式系统中,任务调度器 = 这个管家!
🧱 核心组成:结构体 + 数组 + 循环
1️⃣ 定义任务结构体("任务档案")
c
// scheduler.h
#ifndef __SCHEDULER_H
#define __SCHEDULER_H
#include <stdint.h>
// 一个任务 = 函数 + 周期 + 上次执行时间
typedef struct {
void (*task_func)(void); // 函数指针:要做的事
uint32_t rate_ms; // 执行周期(毫秒)
uint32_t last_run; // 上次执行的时间戳
} task_t;
void scheduler_init(void);
void scheduler_run(void);
#endif
💡 结构体就像"汽车档案":把品牌、型号、年份打包在一起,管理更方便!
2️⃣ 创建任务列表("管家的任务表")
c
// scheduler.c
#include "scheduler.h"
#include "main.h" // 包含 LED、按键等函数声明
// 声明具体任务函数
void led_toggle_task(void);
void key_scan_task(void);
// 任务数组:定义所有要做的事
static task_t tasks[] = {
{led_toggle_task, 500, 0}, // 每500ms翻转LED
{key_scan_task, 10, 0}, // 每10ms扫描按键
// {motor_control, 100, 0}, // 可扩展更多任务
};
// 自动计算任务数量
static const uint8_t task_count = sizeof(tasks) / sizeof(tasks[0]);
3️⃣ 实现任务函数("具体要做的事")
c
// 用户自定义任务
void led_toggle_task(void) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 翻转板载LED
}
void key_scan_task(void) {
// 伪代码:读取按键状态
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// 按键按下,做点什么...
}
}
4️⃣ 调度器核心逻辑("管家查表")
c
// 初始化:其实这里不需要做啥(任务已静态定义)
void scheduler_init(void) {
// 可在此处初始化 last_run = HAL_GetTick(),避免上电瞬间全触发
for (int i = 0; i < task_count; i++) {
tasks[i].last_run = HAL_GetTick();
}
}
// 调度主循环:每调用一次,检查所有任务是否该执行
void scheduler_run(void) {
uint32_t now = HAL_GetTick(); // 获取当前系统时间(ms)
for (uint8_t i = 0; i < task_count; i++) {
// 判断:现在是不是该执行这个任务了?
if (now - tasks[i].last_run >= tasks[i].rate_ms) {
tasks[i].last_run = now; // 更新上次执行时间
tasks[i].task_func(); // 执行任务!
}
}
}
✅ 为什么用
now - last_run >= rate_ms?避免
SysTick溢出(49天后HAL_GetTick()会回绕),此写法天然抗溢出!
🧪 在 main() 中使用调度器
c
// main.c
int main(void) {
HAL_Init(); // HAL库初始化(含SysTick)
system_init(); // 我们的系统初始化
scheduler_init(); // 调度器初始化
while (1) {
scheduler_run(); // 不断检查并执行任务
// 注意:这里没有 delay!调度器自己管时间
}
}
✅ 优势:
- 多个任务互不干扰
- 时间精准(依赖 SysTick)
- 代码模块化、易扩展
🔧 三、关键工具:HAL_GetTick() 是什么?
-
功能 :返回系统启动后经过的毫秒数(从 0 开始计数)
-
原理:SysTick 定时器每 1ms 中断一次,内部计数器 +1
-
用途
:
- 任务调度(如本例)
- 测量时间间隔:
start = HAL_GetTick(); ... end = HAL_GetTick(); duration = end - start; - 超时判断:
if (HAL_GetTick() - start > 1000) { timeout! }
c
uint32_t HAL_GetTick(void); // 返回值范围:0 ~ 4294967295(约49.7天)
⚠️ 注意:49.7天后会自动回绕到0 ,但
now - last_run的比较依然有效!
🧠 本章口诀总结
🔌 初始化三步走 :
开时钟 → 配模式 → 设电平!
⏰ 调度器像管家 :
任务列成表,时间到了就执行!
🧱 结构体是宝盒 :
把函数、周期、时间,统统装进一个包!
⚡ 时间用差值比 :
now - last >= rate,不怕溢出稳如狗!🔄 主循环只一行 :
scheduler_run();------ 万事交给它!
🔗 延伸学习
- 【从计算机底层认识指针!深入理解C语言指针!】
👉 https://www.bilibili.com/video/BV1o8411T7K5
✅ 动手试试!
- 修复你的
system_init(),正确配置 LED 引脚 - 实现一个调度器,让 LED 每 1 秒闪烁一次
- 添加一个按键扫描任务,短按开灯,长按关灯
有了系统初始化 + 任务调度器,
你就拥有了一个可扩展、可维护、专业级 的嵌入式骨架!
下一步:往里面填业务逻辑吧!🚀