嵌入式靠干不靠学:从GPIO点灯到Linux驱动实战

嵌入式STM32GPIO
于 2026-08-29 03:56:23 修改
·本内容遵循CC 4.0 BY-SA版权协议

很多初学者刚接触嵌入式的时候,都会有一个感觉:明明看了很多教程、收藏了很多学习路线、甚至把《嵌入式Linux应用开发完全手册》翻了一半,但真到自己写代码、调板子的时候,还是无从下手。这个问题很普遍,也恰恰印证了一个观点——嵌入式是靠“干”出来的,不是靠“学”出来的。

这里的“学”当然不是指完全不用学,而是说,嵌入式是一门工程学科,它的知识体系必须通过动手实践才能真正内化。光看视频、光背概念、光刷八股文,是学不会嵌入式的。你只有亲手写过寄存器、调通过一个串口、把一个驱动从编译错误改到跑起来,才算真正掌握了一部分。

这篇文章我想围绕“嵌入式靠干不靠学”这个主题,结合我平时做项目、带新人的经验,把嵌入式学习过程中“该干什么、怎么干、会遇到什么坑、如何一步步从点灯到Linux驱动”完整梳理一遍。内容偏实战,会给出可直接复制的代码、命令和排查思路,希望能帮你少走弯路。

1. 为什么说嵌入式靠的是“干”而不是“学”

1.1 嵌入式学习最常见的误区

我见到的初学者,一般会经历这么几个阶段:

  1. 买了一堆书和开发板,收藏了几十篇学习路线文章。
  2. 跟着视频把开发环境装好,编译了第一个工程,点了个LED。
  3. 然后开始刷《嵌入式Linux应用开发》的章节,读“字符设备驱动框架”。
  4. 读了两周,发现代码看懂了,但脱离教程自己写一个,完全写不出来。
  5. 于是开始怀疑自己不合适,最终放弃。

这个过程的问题出在哪?在于把嵌入式当成了“知识类学科”来学。你确实背下了“file_operations 结构体包含 open、read、write 等函数指针”,你也知道“ioctl 是用来发控制命令的”,但这些是知识,不是能力。

知识可以通过阅读获得,能力必须通过实践获得。嵌入式恰恰是一个极度依赖能力的领域。

1.2 “学”与“干”的本质区别

“学”是指输入,比如看书、看视频、听课;“干”是指输出,比如写代码、改代码、调试代码、看原理图、测波形、查datasheet。

如果你学完一个知识点,没有对应的“输出物”,那这个知识点很快就会忘。举个例子,你在书上看懂了GPIO输出模式的四种配置——推挽输出、开漏输出、复用推挽、复用开漏,但你没有自己动手把推挽改成开漏、用万用表量过引脚电平变化,那你对这四个概念的理解就停留在字面意思。

相反,如果你在调一个I2C传感器时,发现SDA线一直拉不高,后来查出来是忘了开上拉电阻,然后你亲手把代码里的开漏模式改成推挽模式、或者外加了上拉电阻,问题解决。这时候你对“开漏输出”的理解,会比看书一百遍都深刻。

这就是“干”的价值:它让你在真实的问题中建立知识之间的连接。

1.3 “干”具体指什么

这篇文章里说的“干”,不是指无脑复制别人的代码,而是指下面这些行为:

  • 自己动手写代码,哪怕是最简单的LED闪烁。
  • 遇到编译错误,自己读日志、查资料、改代码,而不是直接问群友。
  • 用开发板做真实的小项目,比如温湿度采集、环境监测、智能小车。
  • 遇到硬件问题,学会用万用表、示波器、逻辑分析仪去定位。
  • 看完一个知识点后,立刻去写一个最小demo验证它。
  • 记录自己的调试过程,形成自己的排查手册。

只有把这些行为变成习惯,嵌入式这条路才算真正走对了。

2. 嵌入式工程师的能力拼图

在开始“干”之前,我们得先搞清楚,嵌入式工程师到底需要哪些能力。很多人只知道嵌入式要学C语言、要学Linux,但具体学什么、学到什么程度,心里没有数。下面我把核心能力拆开讲。

2.1 硬件基础与电路认知

嵌入式不是纯软件,它必须和硬件打交道。哪怕你做的是纯应用层开发,也得看得懂原理图、知道I2C和SPI引脚在哪、知道串口电平是TTL还是RS232。

需要掌握的硬件知识包括:

  • 看懂原理图:电源、晶振、复位电路、LED、按键、排针。
  • 知道常用芯片的引脚功能:STM32的PA0、PB1等引脚能复用成什么功能。
  • 会用万用表:测电压、测通断。
  • 了解基本的电平标准:TTL电平、CMOS电平、RS485、CAN。
  • 会查datasheet:知道怎么找芯片的寄存器地址、时序图。

很多做纯软件的人觉得硬件是硬件工程师的事,但在嵌入式领域,软硬分工没有这么清晰。你写驱动时,如果连原理图上LED接在哪个GPIO都不知道,那根本没法写。

2.2 C语言与底层编程

C语言是嵌入式的母语,这一点不用多说。但要注意,嵌入式的C语言和做纯软件开发的C语言,侧重点不太一样。嵌入式C更关注:

  • 指针:尤其是寄存器地址映射、函数指针、指针与数组的关系。
  • 位操作:与、或、取反、左移、右移,这是配置寄存器的基础。
  • 结构体与联合体:常用于访问寄存器组、协议报文解析。
  • 内存管理:栈、堆、静态区的区别,嵌入式环境内存有限。
  • 编译与链接:编译过程、链接脚本、.map文件、elf文件。

下面是一个典型的结构体+位域访问寄存器的示例:

C
// 文件路径:test_stm32.c
// 用结构体映射GPIO寄存器组,这在很多HAL库源码里都能看到类似思路
 
typedef struct
{
volatile uint32_t CRL; // 偏移0x00:端口配置低寄存器
volatile uint32_t CRH; // 偏移0x04:端口配置高寄存器
volatile uint32_t IDR; // 偏移0x08:端口输入数据寄存器
volatile uint32_t ODR; // 偏移0x0C:端口输出数据寄存器
volatile uint32_t BSRR; // 偏移0x10:端口置位/复位寄存器
volatile uint32_t BRR; // 偏移0x14:端口复位寄存器
volatile uint32_t LCKR; // 偏移0x18:端口配置锁定寄存器
} GPIO_TypeDef;
 
# define GPIOB_BASE 0x40010C00
# define GPIOB ((GPIO_TypeDef *)GPIOB_BASE)
 
int main(void)
{
// 将GPIOB第0位设置为1:点亮LED(前提是PB0接了LED)
GPIOB->BSRR = (1 << 0);
// 将GPIOB第0位复位为0:熄灭LED
GPIOB->BRR = (1 << 0);
return 0;
}

看到没有,所谓操作寄存器,本质就是往一块特定的内存地址上写数据。理解了这一点,你就能看懂HAL库源码里那些GPIOB->BSRR = ...的本质。

2.3 工具链与调试手段

嵌入式开发离不开工具。这里说的工具不只是编译器,还包括调试器、下载器、逻辑分析仪等。常用的工具链如下:

工具 常见选择 用途
开发IDE Keil MDK、STM32CubeIDE、VS Code + GCC 编写和编译代码
下载调试器 ST-Link、J-Link、CMSIS-DAP 烧录程序、在线调试、断点调试
串口工具 SecureCRT、MobaXterm、串口助手 查看日志、调试信息交互
逻辑分析仪 Saleae逻辑分析仪、国产24MHz/100MHz 分析UART、SPI、I2C时序
示波器 台式示波器、便携示波器 观测信号波形、测量频率占空比
版本管理 Git、GitHub、Gitee 代码版本管理和协作

我的建议是,早期不需要买太贵的设备。一个STM32F103C8T6最小系统板(十几块钱)、一个ST-Link、一根USB转TTL串口线,再加上一个几十块钱的逻辑分析仪,就足够完成大部分入门阶段的调试。

2.4 从裸机到RTOS再到Linux

嵌入式开发有三个层次,很多初学者分不清:

  • 裸机开发:程序直接跑在处理器上,没有操作系统。最典型的应用是51单片机、STM32的裸机工程。
  • RTOS(实时操作系统):程序跑在一个小型操作系统上,比如FreeRTOS、RT-Thread,任务调度由系统完成。
  • 嵌入式Linux:在带MMU的应用处理器上运行完整Linux内核,比如树莓派、i.MX6ULL、全志芯片。

这三个层次对能力的要求是递进的。裸机开发要求你懂寄存器、懂中断、懂定时器;RTOS要求你理解任务调度、信号量、消息队列;Linux方向则要求你懂系统编程、内核驱动、交叉编译、文件系统。

这里想特别提醒一句:不要一上来就学嵌入式Linux,除非你已经有C语言基础和单片机基础。很多新人听网上说“Linux嵌入式工资高”,于是直接从裸机跳到内核学习,结果被设备树、驱动框架、Kconfig这些概念劝退。正确的路线是先玩透单片机,再过渡到RTOS,最后再考虑Linux。

3. 动手前的准备:硬件与软件环境

3.1 开发板怎么选

如果你还在纠结买哪块板子,我给你一个比较稳妥的建议:

  • 入门阶段:STM32F103C8T6最小系统板,或者STM32F103ZET6正点原子/野火开发板。网上资料最多,遇到问题最容易搜到解决方案。
  • 进阶阶段:ESP32开发板。带WiFi蓝牙,适合做物联网项目,也支持FreeRTOS。
  • Linux方向:正点原子/韦东山的i.MX6ULL板子,或者树莓派。前者资料全、适合学驱动,后者适合做应用层和边缘计算项目。

不建议一上来就买好几块板子,一块板子用透,比买三块板子吃灰有用得多。开发板只是载体,真正值钱的是你拿它做了什么项目。

3.2 必备软件工具

以STM32为例,开发环境如下:

BASH
# Windows下常见方案
Keil MDK5 # 编译调试STM32工程
STM32CubeMX # 图形化配置引脚、时钟、外设,生成初始化代码
ST-Link驱动 # 连接开发板和PC
 
# Linux下常见方案
sudo apt-get install gcc-arm-none-eabi # ARM交叉编译器
sudo apt-get install openocd # 开源调试下载工具
sudo apt-get install stlink-tools # ST-Link命令行工具

这里具体用什么IDE,取决于你的习惯。我个人的建议是:入门阶段用Keil MDK,因为教程多、资料全,遇到问题好解决。等你熟悉了嵌入式开发的流程之后,再迁移到STM32CubeIDE或者VSCode + GCC的命令行编译方式。

3.3 调试硬件推荐

调试硬件不需要一步到位,按需购买即可:

  • 刚入门:一个LED、一个按键、一块面包板、若干杜邦线、一个万用表。
  • 学串口:一根USB转TTL的串口线,CH340芯片的就行,自带驱动。
  • 学协议时序:一个逻辑分析仪,8通道24MHz的足够用。
  • 学模拟电路:一个示波器,预算有限就买二手的。

很多人舍不得买逻辑分析仪,觉得用示波器就够了。但逻辑分析仪在分析I2C、SPI、UART这类数字协议时,比示波器好用得多,而且价格便宜,强烈推荐。

4. 第一个实战:从GPIO点灯开始

如果把嵌入式比作学游泳,那“点灯”就是漂浮和换气,是入水的第一步。不要觉得点灯太简单,你在点灯过程中会经历完整的嵌入式开发流程:看原理图、配置引脚、写代码、编译、下载、运行、调试。这个过程你走通了,后面的路就顺了。

4.1 需求与硬件连接

需求很简单:让开发板上的一个LED以1秒为周期闪烁。

假设硬件连接如下:

  • LED正极通过一个330欧姆限流电阻接到3.3V电源。
  • LED负极接到STM32的PB0引脚。
  • 那么当PB0输出低电平时LED点亮,输出高电平时LED熄灭。

当然,不同开发板的LED接法不同,有的是引脚输出高电平点亮,有的是输出低电平点亮,所以我建议你看一下自己板子的原理图。如果你用的是正点原子/野火开发板,原理图里都会标注LED的连接方式。

4.2 寄存器方式的点灯代码

先来看最原始的寄存器方式,理解CPU到底在操作什么。以STM32F103为例:

C
// 文件路径:main.c
// 寄存器方式点灯,适用于STM32F103系列,LED接PB0
 
# define RCC_APB2ENR (*(volatile unsigned int *)0x40021018)
# define GPIOB_CRL (*(volatile unsigned int *)0x40010C00)
# define GPIOB_ODR (*(volatile unsigned int *)0x40010C0C)
 
void delay_ms(volatile unsigned int ms)
{
volatile unsigned int i;
while (ms--) {
for (i = 0; i < 7200; i++);
}
}
 
int main(void)
{
// 1. 打开GPIOB的时钟,GPIOB挂载在APB2总线上,bit3置1
RCC_APB2ENR |= (1 << 3);
 
// 2. 配置PB0为推挽输出,速度2MHz
// PB0对应CRL寄存器的bit[3:0],模式位为0010(输出2MHz)
GPIOB_CRL &= ~(0x0F << 0);
GPIOB_CRL |= (0x02 << 0);
 
// 3. 循环翻转PB0的输出电平
while (1) {
GPIOB_ODR &= ~(1 << 0); // PB0输出低电平,LED点亮
delay_ms(500);
GPIOB_ODR |= (1 << 0); // PB0输出高电平,LED熄灭
delay_ms(500);
}
}

这段代码很简单,但里面有几个点值得反复琢磨:

  • (volatile unsigned int *)0x40010C00是地址强转,意味着把这个地址当成一个整型指针。
  • 前面的*是解引用,表示往这个地址里写入数据。
  • volatile告诉编译器不要对这个地址的读写做优化,因为它的值可能随时被硬件改变。

理解了这几行,你再去读HAL库的源码,会发现很多宏定义的本质就是这个。

4.3 HAL库方式的点灯代码

如果你用的是STM32CubeMX生成的工程,代码会简洁一些。下面是HAL库方式的点灯逻辑:

C
// 文件路径:Core/Src/main.c(核心逻辑)
# include "main.h"
 
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
 
__HAL_RCC_GPIOB_CLK_ENABLE();
 
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
 
int main(void)
{
HAL_Init();
MX_GPIO_Init();
 
while (1)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 点亮LED
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 熄灭LED
HAL_Delay(500);
}
}

HAL库的好处是代码可读性好、可移植性强,但你第一次用的时候,一定要去HAL库源码里查一下HAL_GPIO_WritePin到底做了什么操作。把这个函数定义找出来,你会发现它最终还是往寄存器里写值。

4.4 编译、下载与验证

在Keil MDK中,操作流程如下:

  1. 新建工程,选择芯片型号STM32F103C8。
  2. 添加main.c源文件。
  3. 配置工程选项:选择ST-Link下载器、配置Flash Download算法。
  4. 编译工程。
  5. 连接ST-Link和开发板,点击Download烧录。
  6. 观察LED是否以1Hz频率闪烁。

如果你用的是命令行方式,可以这样操作:

BASH
# 以STM32CubeCLT为例,编译并下载
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -o build/main.elf main.c startup_stm32f103.s
st-flash write build/main.elf 0x08000000

预期结果:开发板上的LED以1秒为周期亮灭切换。

点灯这个任务本身很简单,但它背后体现的是整个嵌入式开发流程。我个人建议你点灯成功后,再自己做几个小变化:

  • 把点亮LED的任务改成按键控制(用GPIO输入)。
  • 把两个LED做成交替闪烁。
  • 用逻辑分析仪观察PB0引脚的波形,看频率是否准确。

5. 第二个实战:中断、定时器与串口

点灯做完之后,你已经掌握了GPIO输出的基本用法。接下来要攻克的是嵌入式开发的三大基石:中断、定时器和串口通信。

5.1 按键中断:从轮询到事件驱动

刚开始学按键,很多人会用轮询的方式,也就是在主循环里持续读取按键引脚的电平:

C
while (1)
{
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
{
HAL_Delay(20); // 软件消抖
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
}
}
}

这种方式简单,但CPU要一直跑主循环,大部分时间都在空转。更合理的方式是用外部中断:按键按下时,硬件触发中断,CPU暂停当前任务,跳到中断服务函数执行,执行完再返回。

C
// 文件路径:按键中断回调
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_1)
{
HAL_Delay(20); // 简单消抖,真实项目建议用定时器消抖
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
}
}
}

这个例子看起来只是把轮询换成了中断,但它体现了一个重要的架构思想:不要在轮询中等待,让事件主动来找你。

提到事件驱动,现在嵌入式领域有一个很明显的趋势:从“超级大循环”架构,往“事件驱动”架构升级。以前一个裸机程序就是while(1)里“初始化+判断标志+执行任务”,所有任务按顺序跑;现在更推荐用事件驱动的方式,把任务拆成事件,通过标志位或状态机来组织程序。这个思路在RTOS和前后台系统里都有广泛的应用,值得你写几个小项目去体会。

5.2 定时器PWM:呼吸灯

定时器的用途非常多:计时、延时、产生PWM、输入捕获、编码器接口等。其中PWM是最常用的功能之一,可以用来控制LED亮度、电机转速、舵机角度。

下面的代码基于STM32F103,用TIM2的通道1输出PWM,实现LED呼吸灯效果:

C
// 文件路径:Core/Src/tim.c(核心逻辑)
# include "tim.h"
# include "main.h"
 
TIM_HandleTypeDef htim2;
 
void MX_TIM2_Init(void)
{
TIM_OC_InitTypeDef sConfigOC = {0};
 
__HAL_RCC_TIM2_CLK_ENABLE();
 
htim2.Instance = TIM2;
htim2.Init.Prescaler = 720 - 1; // 72MHz / 720 = 100kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 100 - 1; // 100kHz / 100 = 1kHz PWM频率
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&htim2);
 
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0; // 初始占空比为0%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
 
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}
 
void set_led_brightness(uint8_t brightness)
{
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, brightness);
}

主循环里改变占空比,实现呼吸效果:

C
// 文件路径:Core/Src/main.c(核心逻辑)
while (1)
{
for (uint16_t duty = 0; duty <= 100; duty++)
{
set_led_brightness(duty);
HAL_Delay(10);
}
for (uint16_t duty = 100; duty > 0; duty--)
{
set_led_brightness(duty);
HAL_Delay(10);
}
}

这里需要理解PWM的两个关键参数:

  • 频率:由定时器预分频器和自动重载寄存器决定,本例中是1kHz。
  • 占空比:由比较寄存器决定,占总周期的比例,数值越大LED越亮。

明白这两个概念后,你再去看电机调速、舵机控制,会发现底层原理都是一样的。

5.3 串口收发:调试的“眼睛”

嵌入式调试最常用的手段就是串口打印。没有串口日志,你写程序就像闭着眼走路,只能靠猜。

先看一个最简单的串口回环实验:

C
// 文件路径:Core/Src/main.c(核心逻辑)
// 前提:USART1已经通过CubeMX配置好,波特率115200,开启接收中断
 
uint8_t rx_data = 0;
 
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 把收到的字节原样发回去
HAL_UART_Transmit(&huart1, &rx_data, 1, 100);
// 重新使能接收中断,准备接收下一个字节
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
}
 
int main(void)
{
HAL_Init();
MX_GPIO_Init();
MX_USART1_UART_Init();
 
// 启动串口接收中断
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
 
while (1)
{
// 主循环可以干了
}
}

在PC端,你用串口助手打开对应端口,波特率设置为115200,然后发送任意字符,就能收到同样的字符。

这个实验看起来简单,但它包含了一个非常重要的点:串口接收是异步的,你不知道数据什么时候来,因此要用中断,在中断里把数据接收下来,然后再处理。

当你掌握了串口收发之后,可以做一个更有意思的项目:把DHT11温湿度传感器的数据通过串口打印到PC端。这个项目会涉及到单总线时序、延时控制、数据校验、格式化输出,做完之后你对嵌入式通信的理解会上升一个台阶。

5.4 综合小项目:环境监测节点

到了这一步,你可以把学过的东西组合起来,做一个完整的节点功能:

  • 用DHT11采集温湿度。
  • 用STM32读取数据。
  • 通过串口把数据上传到PC,格式化为JSON字符串。
  • 当温度超过阈值时,用LED报警。

下面是核心逻辑的示例思路:

C
// 文件路径:Core/Src/main.c(核心逻辑)
while (1)
{
if (dht11_read(&temperature, &humidity) == 0)
{
char buf[64];
snprintf(buf, sizeof(buf), "{\"temp\":%d.%d,\"hum\":%d.%d}\r\n",
temperature / 10, temperature % 10,
humidity / 10, humidity % 10);
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 1000);
if (temperature > 300) // 30.0度
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 报警LED亮
}
else
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
}
}
HAL_Delay(2000);
}

这个项目麻雀虽小,但五脏俱全:涉及传感器驱动、数据解析、串口通信、条件判断、格式化输出。做完它,你就不再是只会“抄代码”的初学者了。

6. 第三个实战:嵌入式Linux方向

如果你已经玩透了裸机开发,接下来可以考虑嵌入式Linux方向。这是目前嵌入式岗位需求最多的方向之一,包含应用开发和驱动开发两个子方向。

需要说明的是,嵌入式Linux的内容非常庞杂,本节不可能面面俱到,我重点讲两条主线:环境搭建和第一个驱动。

6.1 环境准备与交叉编译

嵌入式Linux开发的第一步是搭建交叉编译环境。所谓交叉编译,就是在PC上编译出目标板能运行的二进制程序。

BASH
# 安装交叉编译工具链(以ARM 32位为例)
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf
 
# 验证版本
arm-linux-gnueabihf-gcc --version
 
# 编译一个hello程序
arm-linux-gnueabihf-gcc -o hello hello.c
 
# 查看生成的文件格式
file hello

看到ELF 32-bit LSB executable, ARM, EABI5 version 1这样的输出,说明编译成功。

在编译Linux内核时,也有几条常用命令:

BASH
# 配置内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- xxx_defconfig
 
# 编译内核镜像
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j4
 
# 编译设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
 
# 编译内核模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

6.2 最小文件系统搭建

编译好内核之后,还需要一个根文件系统,否则Linux无法启动到用户态。做根文件系统的常用工具是BusyBox,它能提供shell和基本的Linux命令。

BASH
# 下载并解压busybox源码
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xjf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
 
# 配置静态编译
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
 
# 在menuconfig界面中设置:Busybox Settings --> Build Options --> Build static binary
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install

安装完成后,会生成_install目录,里面包含bin、sbin、usr等目录。我们再手动创建dev、proc、sys、etc等目录,并添加一个简单的rcS启动脚本:

BASH
# 创建根文件系统目录
mkdir -p rootfs/{dev,proc,sys,etc,lib,usr,var,tmp}
 
# 拷贝busybox安装内容
cp -a busybox-1.36.1/_install/* rootfs/
 
# 创建启动脚本
mkdir -p rootfs/etc/init.d
cat > rootfs/etc/init.d/rcS << 'EOF'
# !/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
mdev -s
EOF
 
chmod +x rootfs/etc/init.d/rcS

然后,使用SD卡或者NFS网络文件系统把这个rootfs挂载到开发板上。至于具体怎么挂载,不同开发板厂商的方式不一样,建议参考你板子的官方文档,Intel/正点原子/韦东山等资料里都有非常详细的步骤。

6.3 一个简单的字符设备驱动

很多初学者一听到“驱动开发”就紧张,觉得它很神秘。其实驱动开发的本质就是:写一段代码,让位于内核态的驱动程序能够响应应用程序的open、read、write等系统调用。下面是一个最小字符设备驱动:

C
// 文件路径:demo_driver.c
// 最小字符设备驱动,提供open接口
# include <linux/module.h>
# include <linux/kernel.h>
# include <linux/fs.h>
# include <linux/init.h>
 
# define DEVICE_NAME "demo_driver"
 
static int major;
 
static int demo_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "demo device opened\n");
return 0;
}
 
static const struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
};
 
static int __init demo_init(void)
{
major = register_chrdev(0, DEVICE_NAME, &demo_fops);
if (major < 0) {
printk(KERN_ERR "register demo driver failed\n");
return major;
}
printk(KERN_INFO "demo driver loaded, major = %d\n", major);
return 0;
}
 
static void __exit demo_exit(void)
{
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_INFO "demo driver unloaded\n");
}
 
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");

对应的Makefile如下:

MAKEFILE
# 文件路径:Makefile
obj-m := demo_driver.o
KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
 
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
 
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

在目标板上加载模块并测试:

BASH
# 加载驱动模块
insmod demo_driver.ko
 
# 查看模块信息
lsmod | grep demo
 
# 查看内核打印日志
dmesg | tail -10
 
# 创建设备节点
mknod /dev/demo_driver c 240 0
 
# 打开设备节点测试(在应用程序中调用open)

这个驱动虽然简单,但它包含了字符设备驱动的核心骨架:注册设备号、填充file_operations、module_init入口、module_exit出口。理解了它,你再去学平台设备驱动、设备树、中断子系统,会有一个比较扎实的基础。

6.4 应用层与网口配置实战

如果你不想做底层驱动,嵌入式Linux应用开发也是一个很不错的方向。应用开发主要做的事情是:在Linux系统上编写C/C++、Python或Qt程序,与硬件设备交互,实现具体的业务逻辑。

应用开发最常用的调试工具是交叉编译后拷贝到板子上运行,或者直接在板子上用GCC编译。除了常规的进程、线程、网络编程之外,嵌入式应用开发经常涉及GPIO控制、串口通信、socket通信等。

这里举一个实际很常见的场景:开发板上有两个网口,但默认只配置了一个,需要手动开启第二个网口。

BASH
# 查看当前网口信息
ifconfig -a
 
# 编辑网络配置文件
sudo vi /etc/network/interfaces
 
# 添加第二个网口的静态IP配置
auto eth1
iface eth1 inet static
address 192.168.1.100
netmask 255.255.255.0
gateway 192.168.1.1
 
# 重启网络服务
sudo /etc/init.d/networking restart
 
# 验证
ifconfig eth1
ping -I eth1 192.168.1.1

如果你的板子用的是systemd,则可以通过systemctl restart networking来重启网络服务。不同Linux发行版网络管理方式不同,实际使用时要先确认板子用的是/etc/network/interfaces还是NetworkManager。

这类问题看起来不起眼,但它非常考察你的排错能力:网口没起来,你要先看ifconfig -a确认有没有识别到硬件,再看dmesg有没有驱动报错,再看配置文件有没有写错,最后才能定位问题在哪。这种排查过程,就是“干”出来的能力。

7. 常见问题与排查思路

写代码最不可避免的就是遇到问题。下面我把嵌入式开发中最高频的问题整理成一个排查表格,方便你收藏备用。

问题现象 常见原因 排查思路
工程编译报错“target not created” 语法错误、头文件缺失、编译器路径不对 双击报错信息定位到具体行,优先看第一个报错;检查include路径
编译报错“undefined reference to xxx” 库文件未添加、函数未实现 检查是否含有对应源文件,检查链接库顺序
下载程序失败“No target connected” ST-Link驱动异常、接线错误、目标板未供电 检查驱动是否安装,检查SWDIO/SWCLK接线,检查板子供电
程序下载成功但板上无反应 启动文件缺失、时钟配置错误、LED引脚不对 检查工程是否包含启动文件;用调试器读取寄存器确认程序是否跑起来
串口输出乱码 波特率不匹配、电平不匹配、晶振频率配置错 检查PC端串口工具波特率;检查程序中的系统时钟是否等于实际晶振
串口收不到数据 接线交叉、收发引脚接反 确认TX接RX、RX接TX;确认共地
按键不灵敏、误触发频繁 未消抖、上拉电阻配置错误 增加消抖延时;配置内部上拉;必要时加RC滤波
I2C设备读取不到数据 地址错误、上拉电阻缺失、时序不匹配 用逻辑分析仪抓取波形,确认SCL/SDA电平是否正常,确认设备地址
Linux板子网口不通 网口未使能、IP配置错误、网线问题 先看ifconfig -a,再ping局域网地址,再检查网关路由
内核模块insmod失败 内核版本不匹配、模块依赖缺失 使用modinfo查看模块信息,确认内核源码版本一致
程序运行一段时间后跑飞 栈溢出、数组越界、中断优先级配置不当 检查代码中的数组访问边界;加大任务栈;用调试器看PC指针位置
嵌入式Linux下Qt程序内存持续增长 内存泄漏、缓冲区未释放、信号槽连接未断开 使用valgrind或top监控内存;检查new出来的对象是否delete;检查定时器是否停止

这些问题,很多你只有在真实项目里才会遇到。遇到不可怕,重要的是养成“现象 -> 假设 -> 验证 -> 结论”的排查思维链条。

8. 工程实践与职业建议

8.1 代码规范与版本管理

嵌入式开发也是软件开发,代码规范同样重要。建议从一开始就养成这些习惯:

  • 命名规范:变量用snake_case、函数用模块_动作、宏定义全大写。
  • 头文件要有头文件保护符#ifndef XXX_H
  • 关键代码必须写注释,注释写“为什么这么干”,而不是“这里干了什么”。
  • 每个工程都要用Git管理,哪怕只是本地仓库。
C
// 头文件保护示例:led.h
# ifndef __LED_H
# define __LED_H
 
# include "stm32f1xx_hal.h"
 
void led_init(void);
void led_on(void);
void led_off(void);
void led_toggle(void);
 
# endif

8.2 调试工具优先于打印

很多初学者遇到Bug,第一反应是到处加printf。这是可以理解的,但不应当成为唯一手段。

嵌入式调试有个“两分钟法则”:如果一个问题你用打印花了两分钟还没定位出来,就应当换工具了。用调试器打断点、单步执行、查看变量值,往往比打印更快。尤其是遇到段错误、硬件异常这类问题,打印基本帮不上忙,必须依赖调试器。

当你做嵌入式Linux开发时,gdb调试、strace跟踪系统调用、perf性能分析、dmesg查看内核日志,这些都是必备技能。

8.3 文档与可维护性

嵌入式项目往往是长期维护的,几个月后你可能还要回头改代码。如果文档缺失,连自己写的代码都很难看懂。

推荐的文档习惯:

  • 每个项目写一个README.md,记录硬件平台、编译方式、烧录方式、已知问题。
  • 遇到疑难Bug,把排查过程记录下来,形成自己的“踩坑记录”。
  • 画系统框图的时候,尽量简洁,可以用表格或文字描述来完成。

8.4 面试项目怎么准备

嵌入式岗位面试,面试官最看重的是项目经验。这里的“项目经验”不一定是工作项目,也可以是你在学习阶段做的实战项目。关键是你要能讲清楚:

  • 项目实现了什么功能。
  • 系统中用了哪些硬件外设,为什么选它。
  • 软件架构是怎么设计的,为什么这么设计。
  • 项目中有没有遇到什么难缠的Bug,怎么解决的。
  • 如果重新做,你会怎么优化。

一个常见的面试项目思路是:“基于STM32的环境监测系统”。你可以把它扩展成一个物联网项目:STM32采集温湿度,通过ESP8266模块用MQTT协议上报到云平台,用户可以从手机端查看数据。这个项目虽然不算复杂,但涉及传感器驱动、串口通信、AT指令、无线网络、云平台对接,覆盖的知识面足够广。

面试官问你项目时,不要只背概念,要讲“做”的过程。比如,你是如何解决ESP8266数据丢失的?你有没有做过缓冲队列?WiFi断线后重连的逻辑是怎么实现的?这些问题你只有真正动手做过,才能回答得具体、真实。

9. 总结与学习路线

回到标题那句话:嵌入式靠的是“干”而不是“学”。

“学”能给你知识体系,“干”才能把知识变成能力。这两者不是二选一,而是以“干”为主、以“学”为辅,用实战驱动学习。你在做项目中遇到不懂的知识点,再去查资料、看手册,这时候的学习效率是最高的。

给你一条比较可行的学习路线:

  • 第一阶段:玩透STM32裸机开发。点灯、按键、中断、定时器、串口、I2C、SPI。每个外设都写一个小demo。
  • 第二阶段:把多个外设组合起来,做一个完整的小项目。比如环境监测节点、智能小车、四轴飞控的控制板部分。
  • 第三阶段:引入RTOS,学习FreeRTOS或RT-Thread,理解任务、信号量、消息队列。
  • 第四阶段:进入嵌入式Linux。先搭建交叉编译环境,再编译内核,再做根文件系统,最后写一个字符设备驱动。
  • 第五阶段:选择细分方向。要么深入研究Linux驱动、要么转向应用开发(Qt/C++)、要么结合AI做嵌入式AI(比如把大模型部署到边缘板子上)。

每一个阶段,都要给自己设定一个“做完一个可演示的东西”的目标。不要只想着“学完再开始做”,而是“边做边学、为做而学”。

希望这篇文章能帮你在嵌入式这条路上少一些迷茫、多一些行动。如果你觉得有收获,可以收藏备用;如果你在实际动手过程中遇到了其他问题,也欢迎在评论区留言,我会尽量帮你分析。嵌入式没有捷径,但也没有那么难,关键看你能不能静下心来,把手上的开发板真正“干”起来。

[野火]《嵌入式Linux驱动开发实战指南——基于i.MX6ULL系列》
下面是从该资源中总结出的知识点1. 嵌入式 Linux 驱动开发概述嵌入式 Linux 驱动开发是指在嵌入式系统中使用 Linux 操作系统来驱动硬件设备的过程。
天南散修
1812
AM5728 gpio点灯入门操作
**初始化GPIO**首先,加载必要的GPIO驱动,这是操作系统(如Linux)的一部分,用于管理硬件资源。在内核配置阶段,确保GPIO子系统被启用,并在启动时加载相应的驱动模块。2.
607
Linux GPIO点灯驱动框架
Linux GPIO点灯驱动框架
极客-杀生丸
180
linux 点灯驱动(带设备树版本)
总的来说,“Linux点灯驱动(带设备树版本)”涵盖了Linux驱动开发的基本流程,包括设备树的使用、驱动模型的选择和实现、以及GPIO的控制。
white_line
332
嵌入式linux点灯qt
本文介绍如何在嵌入式Linux系统中利用Qt开发工具和fcntl.h库实现点灯控制。通过编写代码操作GPIO口,使用O_WRONLY和O_RDONLY宏以及write函数来控制电平状态,从而达到点亮LED灯的目的。
闫&兔子桃乐乐
嵌入式LinuxGPIO驱动程序开发
### 嵌入式LinuxGPIO驱动程序开发#### 一、引言在嵌入式领域中,Linux因其开放性和灵活性而广受欢迎。
235
linux gpio 模拟I2C驱动
驱动允许通过通用输入/输出(GPIO)引脚来模拟 I2C 总线的行为,这对于那些不支持硬件 I2C 的嵌入式系统来说尤为重要。
shitouzaiyunjian
1103
Linux GPIO 驱动模块
**边沿触发和电平触发**可以配置GPIO中断为上升沿、下降沿或电平触发。在嵌入式开发和物联网(IoT)应用中,Linux GPIO驱动常用于控制外围设备,如LED灯、传感器、电机等。
655
基于ARM9的Linux嵌入式字符驱动GPIO设计
本文主要探讨了基于ARM9处理器的嵌入式Linux系统中,如何进行字符驱动的开发,特别是针对GPIO(General Purpose Input/Output)的控制。文章首先阐述了嵌入式Linux
weixin_38707240
175
嵌入式Linux开发GPIO点灯驱动实战
本文系统讲解嵌入式Linux下基于GPIO的LED点灯实践,涵盖GPIO硬件原理、Linux GPIO子系统三层架构(芯片驱动层、核心层、用户接口)、字符设备驱动开发流程(框架搭建与用户空间控制方式),以及设备树配置、常见故障排查(硬件/软件/驱动调试)和性能优化技巧(如快速路径、PWM替代)。内容聚焦驱动开发核心技术链,适用于RK3568、STM32等主流平台。
山清水秀iOS
511
Linux嵌入式开发入门GPIO点灯驱动编写实战
本文系统讲解Linux嵌入式环境下GPIO控制的完整技术链路从用户空间sysfs/WiringPi点灯入门,到编写字符设备驱动、集成设备树与平台驱动,涵盖硬件连接规范、内核模块开发、设备树配置及调试优化。重点解析GPIOLinux设备模型中的角色、驱动框架搭建、权限与电平处理等核心技术要点,适用于IMX6ULL、全志H3等主流嵌入式平台。
二货哈士奇
262
嵌入式Linux驱动开发——汇编点灯
本文详细介绍了在嵌入式Linux环境中,使用正点原子阿尔法开发板进行汇编编程,通过设置时钟、GPIO配置和IO属性来实现点灯功能的过程,包括编译、链接、Makefile管理和代码烧录等步骤。
Meursault639
1186
嵌入式Linux点灯-pinctrl和gpio子系统
本文介绍如何在嵌入式Linux环境下实现LED灯的点亮,包括修改设备树配置、编写驱动程序及应用程序等步骤。从uboot启动过程讲起,直至完成整个点灯流程。
CofCai
1275
嵌入式 Linux:使用设备树驱动GPIO全流程
本文详细介绍了在嵌入式Linux系统中,如何通过设备树配置和GPIO子系统来驱动LED灯。内容包括设备树中添加pinctrl节点和LED设备节点,编写和编译驱动程序,以及如何进行测试。
Eden-Hazard
2919
嵌入式Linux GPIO子系统实战:点灯驱动开发
本文系统讲解Linux GPIO子系统的核心架构与实战应用,涵盖Chip/Desc/Consumer模型、Sysfs与libgpiod双接口控制、GPIO编号映射、输入/中断/去抖处理、设备树配置及控制器驱动开发。重点对比传统Sysfs与现代字符设备接口的优劣,强调libgpiod在安全性、功能性和可维护性上的优势,并提供C语言编程、命令行工具使用及硬件调试全流程指导。
江半
263
嵌入式 Linux 驱动开发:点灯大法
本文围绕嵌入式Linux驱动开发的点灯实验展开。介绍了整体架构流程,包括硬件准备(IMX6ULL开发板、板载LED)和软件准备(Linux内核源码、交叉编译工具链、ubuntu开发环境)。详细说明了编写LED驱动、测试驱动的步骤,最后呈现了通过传入参数控制目标设备的实验现象。
Eden-Hazard
945
手把手用IMX6ULL点灯:从汇编到Qt的5种GPIO控制方式对比
本文详细解析在NXP i.MX6ULL平台上控制GPIO点亮LED的五种技术路径裸机汇编、裸机C语言、Linux Sysfs接口、设备树+内核驱动、Qt应用层。涵盖硬件基础(GPIO Bank划分、IOMUX配置)、各层级实现原理、性能特征及典型应用场景,强调从寄存器操作到底层驱动再到GUI交互的全栈嵌入式开发逻辑,突出Linux嵌入式系统中设备树与驱动协同的设计范式。
845
[Linux字符驱动] LED基于gpio系统点灯
本文介绍了Linux字符驱动中的LED控制,主要探讨如何基于GPIO系统进行LED点灯操作。内容涵盖模块驱动的入口函数、初始化与清理、驱动操作方法的绑定,以及应用程序和Makefile的编写。同时,文章提供了相关Linux驱动知识的推荐阅读。
weixin_43771853
759
RK3288 GPIO点灯驱动开发与实践指南
本文详细阐述基于RK3288平台的GPIO点灯驱动开发全流程,涵盖设备树配置、GPIO寄存器操作、字符设备驱动实现、sysfs与/dev接口设计,并涉及调试技巧、性能优化(如PWM替代、时钟管理)、移植适配(内核版本与平台差异)及进阶功能(呼吸灯、按键联动)。内容聚焦Linux嵌入式驱动开发核心实践,强调硬件控制、软件抽象与系统集成。
今融道
293
笔记IMX6ULL Mini板-基于pinctrl和gpio子系统编写驱动代码点灯
本文围绕嵌入式系统中pinctrl和GPIO子系统展开,介绍了它们的功能,前者管理配置引脚功能,后者用于与外部设备交互。详细阐述了搭建驱动框架、完善LED驱动的步骤,包括获取设备节点、申请IO等,还进行了结果验证,体现了Linux驱动分离与分层思想,简化开发流程。
清风来点灯
1956
嵌入式Linux点灯实战:从Sysfs到字符设备驱动的完整路径
本文系统讲解在i.MX6ULL等ARM平台下,基于嵌入式Linux实现LED控制的完整技术路径从硬件原理图分析、设备树描述,到Sysfs用户空间快速控制,再到标准字符设备驱动开发与加载;涵盖GPIO操作、内核模块编译、设备树节点定义、驱动与硬件解耦等关键技术点,强调生产环境下的可移植性、并发安全与标准子系统集成。
江平舟
316
嵌入式Linux驱动开发之点灯
本文详细介绍了如何使用驱动开发的方式控制LED灯。首先,查看原理图找到LED灯对应的GPIO口,然后了解IMX6UL的GPIO操作,包括时钟控制模块(CCM)、IO复用控制器(IOMUXC)和GPIO模块结构。接着,阐述了如何打开GPIO时钟、设置引脚复用和模式,以及配置GPIO参数。在编程方面,讲解了驱动程序的编写套路,包括ioremap、iounmap、读写寄存器函数的使用。最后,给出了驱动加载、设备节点创建、设备操作测试及驱动卸载的步骤。
m0_61687959
2074
从零构建基于PetaLinux与AXI_GPIO的Zynq嵌入式Linux点灯实战
本文详细介绍了基于Zynq平台、PetaLinux工具链与AXI_GPIO IP核实现嵌入式Linux点灯的完整流程,涵盖Vivado硬件设计(Zynq PS配置、AXI_GPIO添加与地址映射)、PetaLinux工程创建与内核/设备树配置(重点支持axi_gpio驱动)、系统编译部署(生成BOOT.BIN与image.ub)及硬件验证(devmem寄存器操作与sysfs接口控制)。内容聚焦嵌入式Linux在FPGA SoC上的底层驱动开发与软硬协同调试方法。
weixin_33735077
433
Linux驱动开发实战(四)设备树点RGB灯
本文聚焦嵌入式Linux开发,详细讲解在i.MX6平台通过驱动程序控制RGB LED。介绍驱动实现,包括设计思路、关键数据结构等;阐述设备树配置,解析寄存器地址;从原理图分析引脚与GPIO对应关系及电路连接;详解引脚复用机制;最后通过实验完成点灯,体现该开发方式的可移植与可维护性。
小麦嵌入式
1873
嵌入式Linux初探索——点灯背后的驱动层与应用层及其交互
本文深入剖析嵌入式Linux下LED点灯程序的驱动层与应用层交互机制,重点讲解mmap函数在物理地址映射中的作用、ioremap使用规范及用户态与内核态间数据安全传输。通过系统调用write和copy_to_user完成跨层级通信,揭示字符设备驱动GPIO寄存器操作的核心流程。
大志若愚YYZ
1349
韦东山嵌入式linux系列-GPIO 和 Pinctrl 子系统的使用
本文介绍Linux下的GPIO和Pinctrl子系统,重点讲解如何通过设备树配置引脚复用与电气特性,并在驱动中使用标准API操作GPIO。内容涵盖设备树节点定义、pinctrl状态管理、基于descriptor的GPIO接口及LED驱动实例,提升驱动开发效率并实现板级无关性。
StudyWinter
1576
LINUX】i.MX6学习笔记(3) 花式点灯大全 —— 裸机点灯(汇编、C),linux内核点灯,资源分离点灯,设备树点灯,pinctrl点灯,platform点灯 的异同
本文详述了从裸核汇编至Linux内核框架下各种点灯实验的演进过程,包括直接配置寄存器、使用字符设备接口、资源分离、设备树及平台总线等方法,深入探讨了嵌入式系统开发中的关键技术。
taotao830
1115
实现树莓派基于Linux内核驱动GPIO
本文详细讲解了如何在树莓派上通过Linux内核驱动实现GPIO操作,包括设置GPIO模式、点灯控制,以及使用ioremap函数映射物理地址到虚拟地址的过程。涵盖了设备文件编写、代码实现和应用层交互的全过程。
星辰~念
1224