【0基础嵌入式学习日志】Day15:位操作、GPIO 寄存器模拟与 LED 控制

【0基础嵌入式学习日志】Day15:位操作、GPIO 寄存器模拟与 LED 控制

一、前言

今天开始进入嵌入式学习第二阶段。

前 14 天主要完成了一个嵌入式 C 多模块小项目,从最基础的 C 工程逐步扩展到状态机、控制模块和事件统计模块。

Day01:基础 C 工程结构 Day02:函数封装、结构体指针、故障判断 Day03:多文件拆分,.h / .c 配合 Day04:.c → .o → 可执行文件 Day05:Makefile 自动化、模式规则、增量编译 Day06:.d 依赖文件,头文件依赖追踪 Day07:条件编译、DEBUG 开关与日志打印模块 Day08:config.h 配置头文件、阈值宏管理与消除魔法数字 Day09:枚举 enum、系统工作模式与 switch-case 状态打印 Day10:状态机 FSM、系统模式切换与状态机模块封装 Day11:状态机升级、当前状态判断与状态转移日志 Day12:主循环、多周期运行与状态机连续转移验证 Day13:控制执行模块、LED 状态控制与主循环动作输出 Day14:事件统计模块、故障次数记录与嵌入式小项目收尾

Day14 结束后,项目已经具备了一个比较完整的软件框架:

sensor_update() fault_check() state_machine_update() control_update() event_update() system_print()

但是前面 LED 只是一个普通的软件状态,例如:

LED state: 1 LED state: 2

它还没有体现出真实单片机中“寄存器控制某一位”的思想。

所以 Day15 的目标是:用 C 语言变量模拟 GPIO 输出寄存器,并通过位操作控制 LED 对应的那一位。


二、Day15 学习目标

本次 Day15 主要学习以下内容:

  1. 理解位操作在嵌入式中的作用;
  2. 理解BIT(n)宏的含义;
  3. 使用unsigned int模拟 GPIO 输出寄存器;
  4. 新增gpio.hgpio.c模块;
  5. 使用位操作实现 LED 打开、关闭、翻转和读取;
  6. 理解|=&= ~^=&的基本用法;
  7. 将控制模块从普通 LED 状态扩展为 GPIO 寄存器控制。

Day15 的核心目标是从:

LED state: 1

进一步过渡到:

GPIO output reg: 0x00000001 LED pin level: 1

也就是开始理解真实单片机中 GPIO 引脚控制的底层思想。


三、Day15 工程结构

Day15 是在 Day14 基础上继续修改得到的。

工程结构如下:

day15 ├── Makefile ├── include │ ├── config.h │ ├── control.h │ ├── debug.h │ ├── event.h │ ├── fault_code.h │ ├── fault.h │ ├── gpio.h │ ├── sensor.h │ ├── state_machine.h │ ├── system.h │ └── system_type.h ├── src │ ├── control.c │ ├── event.c │ ├── fault.c │ ├── gpio.c │ ├── main.c │ ├── sensor.c │ ├── state_machine.c │ └── system.c └── build └── day15_test

相比 Day14,Day15 主要新增和修改了:

新增 include/gpio.h 新增 src/gpio.c 修改 include/config.h 修改 include/system_type.h 修改 src/control.c 修改 src/system.c 修改 src/main.c 修改 Makefile

其中最核心的是新增的 GPIO 模块:

gpio.h:声明 GPIO 控制函数 gpio.c:实现 GPIO 寄存器模拟和 LED 位操作

四、为什么要学习 GPIO 寄存器模拟?

真实单片机控制 LED,常见有两种方式。

一种是库函数方式,例如 STM32 HAL 库:

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_RESET);HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_5);

另一种是直接寄存器方式,例如:

GPIOA->ODR|=GPIO_PIN_5;GPIOA->ODR&=~GPIO_PIN_5;GPIOA->ODR^=GPIO_PIN_5;

虽然实际项目中经常会使用库函数,但库函数底层最终还是在操作寄存器中的某一位。

因此,即使现在没有真实开发板,也可以先用 C 语言变量模拟寄存器,提前理解:

GPIO 控制的本质是控制某一个寄存器位。

Day15 中使用:

unsignedintgpio_output_reg;

来模拟一个 GPIO 输出寄存器。


五、config.h 增加 GPIO 位操作宏

Day15 在config.h中新增了两个宏:

#defineLED_GPIO_PIN0#defineBIT(n)(1U<<(n))

其中:

#defineLED_GPIO_PIN0

表示 LED 使用 GPIO 输出寄存器的第 0 位。

可以简单理解为:

第 0 位控制 LED。

而:

#defineBIT(n)(1U<<(n))

用于生成第 n 位对应的掩码。

例如:

BIT(0) = 0000 0001 = 0x00000001 BIT(1) = 0000 0010 = 0x00000002 BIT(2) = 0000 0100 = 0x00000004 BIT(3) = 0000 1000 = 0x00000008

这里的1U表示无符号整数 1,<<是左移运算符。

所以:

BIT(LED_GPIO_PIN)

在当前项目中等价于:

BIT(0)

也就是:

0x00000001

它表示 LED 对应的那一位。


六、system_type.h 增加 GPIO 输出寄存器变量

Day15 在SystemStatus结构体中新增:

unsignedintgpio_output_reg;

完整结构体如下:

typedefstruct{intled_state;unsignedintgpio_output_reg;SystemMode mode;SystemMode last_mode;floatvoltage;floatcurrent;floattemperature;unsignedintfault_code;unsignedintcycle_count;unsignedintfault_cycle_count;unsignedintfault_enter_count;}SystemStatus;

其中:

unsignedintgpio_output_reg;

用于模拟 GPIO 输出寄存器。

可以把它理解成一排虚拟开关:

第7位 第6位 第5位 第4位 第3位 第2位 第1位 第0位 0 0 0 0 0 0 0 0

当前规定:

第 0 位控制 LED。

所以:

gpio_output_reg = 0x00000000 表示 LED 灭 gpio_output_reg = 0x00000001 表示 LED 亮

七、新增 gpio.h

include目录下新增gpio.h

代码如下:

#ifndefGPIO_H#defineGPIO_H#include"system_type.h"voidgpio_init(SystemStatus*sys);voidgpio_led_on(SystemStatus*sys);voidgpio_led_off(SystemStatus*sys);voidgpio_led_toggle(SystemStatus*sys);intgpio_led_read(constSystemStatus*sys);#endif

这里包含:

#include"system_type.h"

是因为函数参数中使用了:

SystemStatus*sys

SystemStatus这个结构体定义在system_type.h中。

各函数作用如下:

gpio_init:初始化 GPIO 模拟寄存器 gpio_led_on:打开 LED gpio_led_off:关闭 LED gpio_led_toggle:翻转 LED gpio_led_read:读取 LED 当前电平

八、新增 gpio.c

src目录下新增gpio.c

完整代码如下:

#include"gpio.h"#include"config.h"#include"debug.h"voidgpio_init(SystemStatus*sys){sys->gpio_output_reg=0;DEBUG_PRINT("gpio init done\n");}voidgpio_led_on(SystemStatus*sys){sys->gpio_output_reg|=BIT(LED_GPIO_PIN);DEBUG_PRINT("gpio: LED bit set\n");}voidgpio_led_off(SystemStatus*sys){sys->gpio_output_reg&=~BIT(LED_GPIO_PIN);DEBUG_PRINT("gpio: LED bit clear\n");}voidgpio_led_toggle(SystemStatus*sys){sys->gpio_output_reg^=BIT(LED_GPIO_PIN);DEBUG_PRINT("gpio: LED bit toggle\n");}intgpio_led_read(constSystemStatus*sys){if(sys->gpio_output_reg&BIT(LED_GPIO_PIN)){return1;}return0;}

这个文件是 Day15 的核心。


九、gpio.c 核心代码解释

1. GPIO 初始化

voidgpio_init(SystemStatus*sys){sys->gpio_output_reg=0;DEBUG_PRINT("gpio init done\n");}

这段代码表示:

系统刚开始时,GPIO 输出寄存器清零。

也就是:

gpio_output_reg = 0x00000000

此时 LED 对应的第 0 位是 0,表示 LED 关闭。


2. LED 打开

voidgpio_led_on(SystemStatus*sys){sys->gpio_output_reg|=BIT(LED_GPIO_PIN);DEBUG_PRINT("gpio: LED bit set\n");}

核心语句是:

sys->gpio_output_reg|=BIT(LED_GPIO_PIN);

其中:

BIT(LED_GPIO_PIN)

当前等价于:

BIT(0)

也就是:

0000 0001

|=是按位或赋值,用于把某一位置 1。

如果原来是:

0000 0000

执行后变为:

0000 0001

所以 LED 被打开。


3. LED 关闭

voidgpio_led_off(SystemStatus*sys){sys->gpio_output_reg&=~BIT(LED_GPIO_PIN);DEBUG_PRINT("gpio: LED bit clear\n");}

核心语句是:

sys->gpio_output_reg&=~BIT(LED_GPIO_PIN);

其中:

~BIT(LED_GPIO_PIN)

表示把 LED 对应的位取反,用于生成清零掩码。

当前 LED 在第 0 位,所以:

BIT(0) = 0000 0001 ~BIT(0) = 1111 1110

再通过&=操作,就可以把第 0 位清 0,同时不影响其他位。

如果原来是:

0000 0001

执行后变为:

0000 0000

所以 LED 被关闭。


4. LED 翻转

voidgpio_led_toggle(SystemStatus*sys){sys->gpio_output_reg^=BIT(LED_GPIO_PIN);DEBUG_PRINT("gpio: LED bit toggle\n");}

核心语句是:

sys->gpio_output_reg^=BIT(LED_GPIO_PIN);

^=是按位异或赋值。

它的作用是:

如果这一位原来是 1,就变成 0; 如果这一位原来是 0,就变成 1。

所以它可以用于 LED 翻转。

例如:

0000 0001 翻转后 0000 0000 0000 0000 翻转后 0000 0001

这就可以模拟 LED 闪烁。


5. 读取 LED 电平

intgpio_led_read(constSystemStatus*sys){if(sys->gpio_output_reg&BIT(LED_GPIO_PIN)){return1;}return0;}

核心判断是:

sys->gpio_output_reg&BIT(LED_GPIO_PIN)

它的作用是检查 GPIO 输出寄存器中 LED 对应的那一位是不是 1。

如果是 1,返回:

return1;

如果是 0,返回:

return0;

十、修改 control.c

Day15 修改control.c,让控制模块不仅修改led_state,还调用 GPIO 控制函数。

完整代码如下:

#include"control.h"#include"config.h"#include"debug.h"#include"gpio.h"voidcontrol_update(SystemStatus*sys){switch(sys->mode){caseSYS_MODE_INIT:sys->led_state=LED_STATE_OFF;gpio_led_off(sys);DEBUG_PRINT("control: LED OFF in INIT mode\n");break;caseSYS_MODE_RUN:sys->led_state=LED_STATE_ON;gpio_led_on(sys);DEBUG_PRINT("control: LED ON in RUN mode\n");break;caseSYS_MODE_FAULT:sys->led_state=LED_STATE_BLINK;gpio_led_toggle(sys);DEBUG_PRINT("control: LED BLINK in FAULT mode\n");break;default:sys->led_state=LED_STATE_OFF;gpio_led_off(sys);DEBUG_PRINT("control: LED OFF in UNKNOWN mode\n");break;}}

对应关系为:

INIT 状态 -> gpio_led_off() RUN 状态 -> gpio_led_on() FAULT 状态 -> gpio_led_toggle()

也就是说:

RUN 状态下 LED 常亮; FAULT 状态下 LED 翻转,用来模拟闪烁; INIT 或未知状态下 LED 关闭。

十一、修改 system.c

Day15 在system.c中增加了 GPIO 输出寄存器的初始化和打印。

system_init()中增加:

sys->gpio_output_reg=0;

表示系统启动时模拟 GPIO 输出寄存器清零。

system_print()中增加:

printf("GPIO output reg: 0x%08X\n",sys->gpio_output_reg);printf("LED pin level: %u\n",(sys->gpio_output_reg&BIT(LED_GPIO_PIN))?1U:0U);

其中:

printf("GPIO output reg: 0x%08X\n",sys->gpio_output_reg);

用于用 8 位十六进制格式打印 GPIO 输出寄存器。

例如:

GPIO output reg: 0x00000001

说明第 0 位是 1。

而:

(sys->gpio_output_reg&BIT(LED_GPIO_PIN))?1U:0U

用于判断 LED 对应的 GPIO 位是不是 1。

如果是 1,输出:

LED pin level: 1

如果是 0,输出:

LED pin level: 0

十二、修改 main.c

Day15 在main.c中增加 GPIO 模块头文件:

#include"gpio.h"

并在系统初始化后增加:

gpio_init(&sys);

完整主函数如下:

#include<stdio.h>#include"system_type.h"#include"system.h"#include"sensor.h"#include"fault.h"#include"state_machine.h"#include"control.h"#include"event.h"#include"gpio.h"intmain(void){SystemStatus sys;system_init(&sys);gpio_init(&sys);for(intcycle=0;cycle<5;cycle++){printf("\n===== Cycle %d =====\n",cycle);sensor_update(&sys,cycle);fault_check(&sys);state_machine_update(&sys);control_update(&sys);event_update(&sys);system_print(&sys);}return0;}

Day15 的主循环仍然保持:

sensor_update fault_check state_machine_update control_update event_update system_print

只是control_update()内部开始调用 GPIO 模拟函数。


十三、Makefile 修改

Day15 从 Day14 复制而来,所以需要修改目标文件名。

将:

TARGET = build/day14_test

修改为:

TARGET = build/day15_test

由于 Makefile 中使用了:

SRCS = $(wildcard src/*.c)

所以新增的:

src/gpio.c

会自动参与编译。


十四、编译运行

进入 Day15 目录:

cd/root/Embedded_14Days/day15

清理旧文件:

makeclean

编译:

make

运行:

makerun

为了只观察关键结果,可以执行:

makerun|grep-E"=====|state:|control:|gpio:|GPIO output reg|LED pin level|LED state|System mode"

十五、运行结果

关键运行结果如下:

===== Cycle 0 ===== [DEBUG] state: INIT -> RUN [DEBUG] gpio: LED bit set [DEBUG] control: LED ON in RUN mode LED state: 1 GPIO output reg: 0x00000001 LED pin level: 1 System mode: RUN ===== Cycle 1 ===== [DEBUG] state: RUN keep [DEBUG] gpio: LED bit set [DEBUG] control: LED ON in RUN mode LED state: 1 GPIO output reg: 0x00000001 LED pin level: 1 System mode: RUN ===== Cycle 2 ===== [DEBUG] state: RUN -> FAULT [DEBUG] gpio: LED bit toggle [DEBUG] control: LED BLINK in FAULT mode LED state: 2 GPIO output reg: 0x00000000 LED pin level: 0 System mode: FAULT ===== Cycle 3 ===== [DEBUG] state: FAULT keep [DEBUG] gpio: LED bit toggle [DEBUG] control: LED BLINK in FAULT mode LED state: 2 GPIO output reg: 0x00000001 LED pin level: 1 System mode: FAULT ===== Cycle 4 ===== [DEBUG] state: FAULT -> RUN [DEBUG] gpio: LED bit set [DEBUG] control: LED ON in RUN mode LED state: 1 GPIO output reg: 0x00000001 LED pin level: 1 System mode: RUN

从运行结果可以看到,GPIO 输出寄存器已经根据系统状态发生变化。


十六、运行结果分析

1. Cycle 0:INIT -> RUN

Cycle 0 中,系统从INIT进入RUN

state: INIT -> RUN

控制模块调用:

gpio_led_on(sys);

GPIO 输出寄存器变为:

GPIO output reg: 0x00000001

说明第 0 位被置 1,LED 当前为亮。


2. Cycle 1:RUN keep

Cycle 1 中,系统继续保持RUN状态:

state: RUN keep

控制模块继续调用:

gpio_led_on(sys);

GPIO 输出寄存器保持:

GPIO output reg: 0x00000001 LED pin level: 1

3. Cycle 2:RUN -> FAULT

Cycle 2 中,传感器数据变为故障值,系统从RUN进入FAULT

state: RUN -> FAULT

控制模块调用:

gpio_led_toggle(sys);

GPIO 输出寄存器从:

0x00000001

翻转为:

0x00000000

LED 当前为灭。


4. Cycle 3:FAULT keep

Cycle 3 中,系统继续保持FAULT状态:

state: FAULT keep

控制模块再次调用:

gpio_led_toggle(sys);

GPIO 输出寄存器从:

0x00000000

翻转为:

0x00000001

LED 当前为亮。


5. Cycle 4:FAULT -> RUN

Cycle 4 中,传感器恢复正常,系统从FAULT恢复到RUN

state: FAULT -> RUN

控制模块调用:

gpio_led_on(sys);

GPIO 输出寄存器保持:

0x00000001

LED 恢复常亮。


十七、今天遇到的问题

今天调试时遇到的主要问题是容易误改其他天的文件。

比如在修改system_type.h时,曾经不小心打开了:

day14/include/system_type.h

而不是:

day15/include/system_type.h

导致 Git 状态中出现了:

modified: day14/include/system_type.h

解决方法是使用:

gitrestore day14/include/system_type.h

恢复误改文件。

这个问题说明:在多天工程目录中修改代码时,必须确认当前文件路径是否正确,尤其是 VS Code 标签页上方显示的路径。


十八、Day15 的核心理解

Day15 最核心的理解是:

GPIO 控制本质上就是控制寄存器中的某一位。

在本项目中:

gpio_output_reg 模拟 GPIO 输出寄存器 LED_GPIO_PIN 表示 LED 使用第 0 位 BIT(LED_GPIO_PIN) 用于找到 LED 对应的那一位

核心位操作为:

reg|=BIT(n);// 把第 n 位置 1reg&=~BIT(n);// 把第 n 位清 0reg^=BIT(n);// 把第 n 位翻转reg&BIT(n);// 读取第 n 位

这四种操作是后面学习 STM32、GD32、ARM 寄存器和外设驱动的基础。

即使以后使用 HAL 库函数,例如:

HAL_GPIO_WritePin();HAL_GPIO_TogglePin();

也需要理解这些库函数底层其实是在操作寄存器位。


十九、今日收获

通过 Day15 的学习,主要掌握了以下内容:

  1. 理解了 GPIO 寄存器模拟的基本思想;
  2. 学会了使用unsigned int模拟 GPIO 输出寄存器;
  3. 理解了LED_GPIO_PINBIT(n)宏的含义;
  4. 学会了使用位操作实现 LED 打开、关闭、翻转和读取;
  5. 理解了|=&= ~^=&在寄存器控制中的作用;
  6. 学会了新增gpio.hgpio.c模块;
  7. 理解了控制模块如何调用 GPIO 模块;
  8. 从普通软件状态控制过渡到了寄存器控制思想。

Day15 的核心可以总结为:

Day14:统计系统运行事件 Day15:用 GPIO 寄存器模拟 LED 控制

这一步标志着第二阶段开始:从纯软件状态机过渡到无硬件外设模拟。


二十、项目源码

本次 Day15 学习代码已上传至 GitHub:

https://github.com/jdai10590-afk/Embedded-C-Learning-Projects/tree/main/day15