工程框架搭建(续)

🧰 二、工程框架搭建(续)

------ 系统初始化 + 超简单任务调度器

💡 想让单片机"听话"?先让它正确启动 ,再给它一个智能管家

本章将带你:

修复并理解 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() 应包含:

  1. 时钟使能(RCC)
  2. GPIO 模式配置(输入/输出/复用)
  3. 外设初始化(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(); ------ 万事交给它!


🔗 延伸学习


✅ 动手试试!

  1. 修复你的 system_init(),正确配置 LED 引脚
  2. 实现一个调度器,让 LED 每 1 秒闪烁一次
  3. 添加一个按键扫描任务,短按开灯,长按关灯

有了系统初始化 + 任务调度器,

你就拥有了一个可扩展、可维护、专业级 的嵌入式骨架!

下一步:往里面填业务逻辑吧!🚀

相关推荐
小菜鸟派大星2 小时前
电路学习(九)MOS管
学习·硬件·mos管·电路·电路仿真
11cookies112 小时前
VSCode + Renode:打造现代化的嵌入式仿真开发环
嵌入式
11cookies112 小时前
我做了一个用 YAML 来驱动串口/TCP 协议执行的框架,上位机从此不需要写代码了
嵌入式
11cookies113 小时前
VSCode + Renode 调试我手工实现的 RTOS:一次彻底改变我开发方式的体验
嵌入式
乔碧萝成都分萝4 小时前
十六、一个基本的GPIO驱动程序
linux·驱动开发·嵌入式
potato_may7 小时前
第三章:LED 模块详解
蓝桥杯·cubemx·嵌入式·led·stm332
不能只会打代码7 小时前
蓝桥杯---垒骰子(Java实现,代码注释,图文讲解)
算法·蓝桥杯·动态规划·垒骰子
沧澜sincerely7 小时前
蓝桥杯11 路径之谜
c++·蓝桥杯·stl·dfs·剪枝
Swift社区1 天前
LeetCode 443. 压缩字符串
leetcode·职场和发展·蓝桥杯