1. 单片机基础巩固:从LED闪烁到寄存器操作,理解MCU的底层工作原理

大家好,我是你们的老朋友。今天咱们聊聊单片机的基础。

很多人学嵌入式,上来就搞Linux驱动,结果遇到中断、内存映射就懵了。为啥?说白了,底层基础没打牢。单片机就是最好的入门砖。你想想看,一个LED闪烁,背后藏着时钟、GPIO、寄存器、位操作——这些都是MCU最核心的东西。

1.1 从点亮一个LED开始

我记得刚入行那会儿,第一个项目就是让LED闪烁。当时觉得简单,不就是给个高电平嘛。但真正做起来,发现门道不少。

先看个最基础的代码:

// 51单片机,点亮P1.0口的LED
#include <reg51.h>

sbit LED = P1^0;

void main() {
    while(1) {
        LED = 0;  // 低电平点亮
        delay(500);
        LED = 1;  // 高电平熄灭
        delay(500);
    }
}

这段代码,初学者都能看懂。但我要问几个问题:

  • P1^0 到底是什么?
  • LED = 0 是怎么让引脚变低的?
  • delay(500) 真的准吗?

嗯,这里要注意。很多新手写了好几年代码,都没搞明白这些问题。我个人习惯,每写一行代码,都要问自己:这行代码在硬件上干了什么?

1.2 寄存器操作——真正控制MCU的方式

上面那个sbit,其实是编译器帮我们封装了。真正的底层操作,是直接读写寄存器。

拿STM32F103来说,点亮PC13引脚上的LED:

// 直接操作寄存器
#define GPIOC_BASE      0x40011000
#define GPIOC_CRH       (*(volatile unsigned int *)(GPIOC_BASE + 0x04))
#define GPIOC_ODR       (*(volatile unsigned int *)(GPIOC_BASE + 0x0C))

void LED_Init(void) {
    // 设置PC13为推挽输出,50MHz
    GPIOC_CRH &= ~(0xF << 20);  // 先清零
    GPIOC_CRH |= (0x3 << 20);   // 设置为推挽输出
}

void LED_On(void) {
    GPIOC_ODR &= ~(1 << 13);   // 低电平点亮
}

void LED_Off(void) {
    GPIOC_ODR |= (1 << 13);    // 高电平熄灭
}

看到没?这才是MCU的本来面目。没有库函数,没有封装,就是直接往内存地址写数据。

核心概念:MCU本质上就是一个特殊的内存映射设备。每个外设(GPIO、定时器、UART)都对应一段内存地址。你往这些地址写数据,就是在控制硬件。

1.3 位操作——嵌入式工程师的基本功

我在项目中遇到过一个问题:一个GPIO口同时控制LED和按键,结果LED闪烁时按键检测失灵。查了半天,发现是位操作没做好,影响了其他位。

位操作有四个基本动作:

操作 C语言写法 说明
置1 REG |= (1 << n) 将第n位置1,其他位不变
清0 REG &= ~(1 << n) 将第n位清0,其他位不变
读取 bit = (REG >> n) & 1 读取第n位的值
翻转 REG ^= (1 << n) 将第n位取反

小技巧:我习惯用宏定义封装位操作,比如 #define SET_BIT(reg, n) (reg |= (1 << n))。这样代码可读性高,也不容易出错。

1.4 时钟系统——MCU的心脏

很多人忽略时钟。但时钟不对,一切都不对。

我曾经调试一个I2C设备,死活通信不上。用示波器一看,SCL时钟频率是110kHz,而我配置的是100kHz。差这么一点,设备就不认了。

时钟配置的典型流程:

  1. 选择时钟源(内部RC、外部晶振、PLL)
  2. 配置分频系数
  3. 等待时钟稳定
  4. 切换系统时钟

以STM32为例,配置系统时钟为72MHz:

void SystemClock_Config(void) {
    // 1. 使能HSE(外部高速晶振)
    RCC->CR |= RCC_CR_HSEON;
    while(!(RCC->CR & RCC_CR_HSERDY));  // 等待HSE就绪
    
    // 2. 配置FLASH预取指
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
    
    // 3. 配置PLL:HSE 8MHz * 9 = 72MHz
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
    RCC->CR |= RCC_CR_PLLON;
    while(!(RCC->CR & RCC_CR_PLLRDY));
    
    // 4. 切换系统时钟为PLL输出
    RCC->CFGR &= ~RCC_CFGR_SW;
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}

注意:时钟切换时,一定要等待就绪标志位。我见过有人没等就绪就往下执行,结果系统跑飞了。这种bug最难查,因为不是每次都复现。

1.5 中断——让MCU学会"响应"

轮询方式太浪费CPU了。比如按键检测,你总不能一直死循环读引脚吧?这时候就需要中断。

中断的配置步骤:

  • 配置NVIC(嵌套向量中断控制器)
  • 使能外设中断
  • 编写中断服务函数

看个外部中断的例子:

// 配置PA0为外部中断,下降沿触发
void EXTI0_Config(void) {
    // 1. 使能时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;
    
    // 2. 配置PA0为输入
    GPIOA->CRL &= ~(0xF << 0);  // 输入模式
    
    // 3. 配置EXTI
    AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0;
    EXTI->IMR |= EXTI_IMR_MR0;     // 不屏蔽
    EXTI->FTSR |= EXTI_FTSR_TR0;   // 下降沿触发
    
    // 4. 配置NVIC
    NVIC_EnableIRQ(EXTI0_IRQn);
    NVIC_SetPriority(EXTI0_IRQn, 1);
}

// 中断服务函数
void EXTI0_IRQHandler(void) {
    if(EXTI->PR & EXTI_PR_PR0) {
        // 处理按键事件
        EXTI->PR = EXTI_PR_PR0;  // 清除中断标志
    }
}

这里有个坑:中断服务函数里,一定要先判断中断标志,再清除。顺序反了,可能会漏掉中断。

1.6 从单片机到Linux的思维转变

学完这些,你会发现:单片机就是一个小型Linux系统。它有CPU、内存、外设,只是资源少得多。

两者的对应关系:

单片机概念 Linux对应 说明
寄存器 设备寄存器(ioremap) 都是通过内存地址访问硬件
中断 中断请求(IRQ) 处理机制类似,Linux更复杂
GPIO GPIO子系统 Linux封装了更高级的接口
定时器 hrtimer、clockevents 精度更高,功能更丰富

说白了,你在单片机上写的每一行寄存器操作,到了Linux里,就是驱动开发的基础。我见过很多Linux驱动工程师,遇到硬件问题就抓瞎,就是因为不懂底层。

我的建议:别急着学Linux驱动。先在单片机上把GPIO、中断、定时器、UART、I2C、SPI这些外设都玩一遍。每个外设都自己写寄存器操作,不要用库函数。等你把MCU摸透了,再去看Linux驱动,会发现很多东西都是相通的。

1.7 实战:用寄存器实现呼吸灯

最后,咱们来个综合练习。用PWM实现呼吸灯效果,全部用寄存器操作:

// 呼吸灯:LED从暗到亮,再从亮到暗
void BreathLight(void) {
    uint16_t duty = 0;
    uint8_t dir = 1;  // 1: 增加, 0: 减少
    
    // 配置定时器2的CH1为PWM输出
    // ...(省略配置代码,读者自行完成)
    
    while(1) {
        // 更新占空比
        TIM2->CCR1 = duty;
        
        // 延时(实际应用中用定时器中断)
        for(volatile uint32_t i = 0; i < 1000; i++);
        
        // 改变占空比方向
        if(dir) {
            duty++;
            if(duty >= 1000) dir = 0;
        } else {
            duty--;
            if(duty == 0) dir = 1;
        }
    }
}

这个例子,把GPIO、定时器、PWM、循环控制都用上了。你能独立写出来,说明单片机基础已经过关了。

好了,这一章就到这里。下一章,咱们聊聊中断的深入应用,以及如何用状态机来管理复杂的程序逻辑。到时候见。