工程框架搭建(续)

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

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

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

本章将带你:

修复并理解 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. 添加一个按键扫描任务,短按开灯,长按关灯

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

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

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

相关推荐
大聪明-PLUS1 天前
如何从零开始开发 Linux 驱动程序
linux·嵌入式·arm·smarc
大聪明-PLUS1 天前
我们如何分析原生应用程序(C++、Windows、Linux)的内存消耗?
linux·嵌入式·arm·smarc
番茄灭世神1 天前
32位ARM单片机视频教程第一篇
arm开发·单片机·嵌入式·gd32·pn学堂
大聪明-PLUS1 天前
工业控制器、Linux 和纯 C++。第一部分
linux·嵌入式·arm·smarc
迷人的星空1 天前
SPI 只是个接口?揭秘芯片间的高速通道!
物联网·嵌入式
怀民民民2 天前
双通道点光源追踪系统
单片机·嵌入式硬件·开源·操作系统·串口·硬件·frtos
大聪明-PLUS2 天前
Linux 网络虚拟化技术的演进
linux·嵌入式·arm·smarc
大聪明-PLUS2 天前
面向开发者的实用 GNU/Linux 命令(第二部分)
linux·嵌入式·arm·smarc
闻缺陷则喜何志丹2 天前
【离线查询 前缀和 二分查找 栈】P12271 [蓝桥杯 2024 国 Python B] 括号与字母|普及+
c++·算法·前缀和·蓝桥杯·二分查找··离线查询
大聪明-PLUS2 天前
管理 Linux 内核模块
linux·嵌入式·arm·smarc