【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 主要学习以下内容:
- 理解位操作在嵌入式中的作用;
- 理解
BIT(n)宏的含义; - 使用
unsigned int模拟 GPIO 输出寄存器; - 新增
gpio.h和gpio.c模块; - 使用位操作实现 LED 打开、关闭、翻转和读取;
- 理解
|=、&= ~、^=、&的基本用法; - 将控制模块从普通 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: 13. Cycle 2:RUN -> FAULT
Cycle 2 中,传感器数据变为故障值,系统从RUN进入FAULT:
state: RUN -> FAULT控制模块调用:
gpio_led_toggle(sys);GPIO 输出寄存器从:
0x00000001翻转为:
0x00000000LED 当前为灭。
4. Cycle 3:FAULT keep
Cycle 3 中,系统继续保持FAULT状态:
state: FAULT keep控制模块再次调用:
gpio_led_toggle(sys);GPIO 输出寄存器从:
0x00000000翻转为:
0x00000001LED 当前为亮。
5. Cycle 4:FAULT -> RUN
Cycle 4 中,传感器恢复正常,系统从FAULT恢复到RUN:
state: FAULT -> RUN控制模块调用:
gpio_led_on(sys);GPIO 输出寄存器保持:
0x00000001LED 恢复常亮。
十七、今天遇到的问题
今天调试时遇到的主要问题是容易误改其他天的文件。
比如在修改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 的学习,主要掌握了以下内容:
- 理解了 GPIO 寄存器模拟的基本思想;
- 学会了使用
unsigned int模拟 GPIO 输出寄存器; - 理解了
LED_GPIO_PIN和BIT(n)宏的含义; - 学会了使用位操作实现 LED 打开、关闭、翻转和读取;
- 理解了
|=、&= ~、^=、&在寄存器控制中的作用; - 学会了新增
gpio.h和gpio.c模块; - 理解了控制模块如何调用 GPIO 模块;
- 从普通软件状态控制过渡到了寄存器控制思想。
Day15 的核心可以总结为:
Day14:统计系统运行事件 Day15:用 GPIO 寄存器模拟 LED 控制这一步标志着第二阶段开始:从纯软件状态机过渡到无硬件外设模拟。
二十、项目源码
本次 Day15 学习代码已上传至 GitHub:
https://github.com/jdai10590-afk/Embedded-C-Learning-Projects/tree/main/day15