从零到一:单片机基础深度解析与实战指南

摘要: 单片机(Microcontroller Unit, MCU)作为现代电子设备和嵌入式系统的核心,是每一位电子工程师、嵌入式开发者乃至创客必须掌握的基础。本文将从单片机的基本概念、内部结构出发,深入剖析其工作原理,并通过大量基于ARM Cortex-M(以STM32为例)和经典8051架构的实战代码,详解GPIO、中断、定时器、串口通信、ADC/DAC等核心功能模块。最后,我们将整合所学,完成一个综合性的实战项目。本文内容详实,代码丰富,旨在为读者构建一个完整、清晰、可动手实践的单片机知识体系。

关键词: 单片机;MCU;STM32;8051;嵌入式系统;C语言;GPIO;中断;定时器;UART


第一章:单片机世界初探

1.1 什么是单片机?

单片机,顾名思义,就是将一个完整的计算机系统集成到一块硅片上。这个微型计算机系统通常包含:

  • 中央处理器(CPU): 负责执行指令、处理数据。
  • 存储器(Memory)
    • 只读存储器(ROM/Flash): 用于存储固件、程序代码。
    • 随机存取存储器(RAM): 用于程序运行时的临时变量和数据存储。
  • 输入/输出接口(I/O Ports): 连接外部世界(如LED、按键、传感器、显示屏)的桥梁。
  • 定时器/计数器(Timer/Counter): 用于精确计时、产生PWM波等。
  • 串行通信接口: 如UART, I2C, SPI,用于与其他芯片或计算机通信。
  • 模数/数模转换器(ADC/DAC): 连接模拟世界与数字世界的通道。

核心特点: 高集成度、体积小、功耗低、可靠性高、成本低廉,专为控制任务而设计。

1.2 单片机 vs 微处理器

许多人容易混淆单片机(MCU)和微处理器(MPU,如电脑的CPU)。

  • 单片机(MCU): “麻雀虽小,五脏俱全”。它本身就是一个最小计算机系统,上电后配合简单的外围电路(如电源、晶振)即可独立工作。强调控制
  • 微处理器(MPU): 它是一个更强大的计算核心,但需要外部搭配RAM、ROM、总线控制器等才能构成一个可工作的计算机系统。强调运算

简单说,MPU是大脑,需要外接器官和四肢;MCU是集成了大脑、小脑和部分神经的完整生物

1.3 主流单片机架构与选型

  1. 基于Intel MCS-51的8位单片机

    • 代表: AT89C51/S51, STC89C52RC。经典、教学首选
    • 特点: 架构简单,资料丰富,易于理解单片机原理,适合入门。
  2. ARM Cortex-M系列32位单片机

    • 代表: STM32(意法半导体), GD32(兆易创新), Nordic nRF52(低功耗蓝牙)。
    • 特点: 性能强大(主频从几十MHz到几百MHz),外设丰富,生态完善,是当前工业界和消费电子的绝对主流。本文将以STM32F103C8T6(又称”Blue Pill”)为例。
  3. 其他架构

    • AVR: Atmel公司产品,Arduino Uno的核心(ATmega328P),创客圈流行。
    • PIC: Microchip公司产品,在工业控制、汽车电子中应用广泛。
    • RISC-V: 开源指令集架构,新兴势力,未来可期。

初学者建议路线: 为了深刻理解原理,可以从8051开始;为了紧跟技术潮流并投入实用,应迅速转向ARM Cortex-M


第二章:深入单片机内部:硬件结构与最小系统

2.1 单片机内部核心框图

以STM32F103为例,其内部结构复杂但高度模块化。理解下图是理解其所有功能的基础:

STM32F103 微控制器

ARM Cortex-M3 Core

AMBA Bus Matrix

Flash Memory

SRAM

AHB to APB Bridge

APB1 Peripherals
TIM2-4, USART2-3, I2C1-2, SPI2

APB2 Peripherals
GPIOA-G, ADC1, USART1, SPI1, TIM1

外设接口

LED/按键

传感器

UART to PC

关键组件

  • 总线矩阵: 协调CPU、DMA、各外设对存储器和外设的访问,是高速数据流通的“交通枢纽”。
  • 存储器: Flash存放代码,SRAM存放运行时的堆、栈、全局变量等。
  • 外设: 挂在APB总线上的各种功能模块,是我们编程控制的主要对象。

2.2 搭建最小系统

要让一块“裸”的单片机跑起来,必须为其提供最基本的“生存条件”,这就是最小系统

STM32F103C8T6最小系统原理图(关键部分)

渲染错误: Mermaid 渲染失败: No diagram type detected matching given configuration for text: circuit LR subgraph MCU_STM32[MCU: STM32F103C8T6] pin3_3v3(3.3V) pin5_BOOT0(BOOT0) pin6_NRST(NRST) pin8_OSC_IN(OSC_IN) pin9_OSC_OUT(OSC_OUT) pin44_VDD(VDDA) pin63_VSS(VSSA) end subgraph Power[3.3V Power] VCC_3V3 end subgraph Reset[Reset Circuit] R1(10kΩ) --上拉--> VCC_3V3 R1 --连接到--> pin6_NRST pin6_NRST --串联--> C1(100nF) C1 --接地--> GND SW1[Reset Button] --并联在C1两端--> SW1 end subgraph Oscillator[8MHz Crystal] XTAL[8MHz Crystal] XTAL_P1 --连接--> pin8_OSC_IN XTAL_P2 --连接--> pin9_OSC_OUT C2(20pF) --一端接OSC_IN, 另一端接地--> GND C3(20pF) --一端接OSC_OUT,另一端接地--> GND end subgraph Boot[Boot Mode] pin5_BOOT0 --下拉--> R2(10kΩ) R2 --接地--> GND “BOOT1 (通过软件设置)” --通常内部下拉-- end VCC_3V3 --> pin3_3v3 VCC_3V3 --> pin44_VDD GND --> pin63_VSS style MCU_STM32 fill:#e1f5fe style Power fill:#f1f8e9 style Reset fill:#fff3e0 style Oscillator fill:#fce4ec

解读

  1. 电源(3.3V): 大多数STM32引脚耐压5V,但核心电压是3.3V,务必使用3.3V稳压器供电(如AMS1117-3.3)。VDDA和VSSA是模拟部分的电源,通常接一个滤波电感或磁珠后与数字电源相连。
  2. 复位电路: 由电阻、电容和按键组成。上电时,电容充电使NRST引脚保持一段时间低电平,实现上电复位。按键提供手动复位。
  3. 晶振电路: 外部8MHz晶振(配合两个负载电容)为系统提供精准时钟源。STM32内部有PLL(锁相环)可以将其倍频至72MHz(STM32F103的最大系统时钟)。
  4. 启动模式(BOOT)
    • BOOT0=0, BOOT1=x: 从用户Flash启动,正常模式。
    • BOOT0=1, BOOT1=0: 从系统存储器启动,用于串口下载(ISP)。
    • BOOT0=1, BOOT1=1: 从内置SRAM启动,用于调试。

对于8051(如STC89C52RC)最小系统

  • 电源: 5V。
  • 晶振: 11.0592MHz或12MHz。
  • 复位电路: 高电平复位(与STM32相反)。
  • EA引脚: 接高电平,使用内部程序存储器。

第三章:软件开发环境搭建与第一个程序

3.1 开发流程与工具链

单片机开发遵循标准的嵌入式开发流程:

  1. 编写代码: 在PC上使用编辑器或IDE(集成开发环境)编写C/C++/汇编代码。
  2. 编译/链接: 使用交叉编译器将源代码转换成单片机可执行的机器码(Hex/Bin文件)
  3. 下载/调试: 通过下载器/调试器(如ST-Link, J-Link, USB-TTL)将机器码烧录到单片机的Flash中,并可进行单步调试。

3.2 ARM (STM32) 开发环境:Keil MDK

以Keil MDK(ARM版)为例,这是最流行的商业IDE之一。

项目创建与配置步骤(精简)

  1. Project -> New uVision Project,选择芯片型号STM32F103C8
  2. 管理运行时环境(RTE),添加Device -> StartupDevice -> StdPeriph Drivers(如果使用标准库)。
  3. 配置魔术棒(Options for Target):
    • Target: 晶振频率设为8.0
    • Output: 勾选Create HEX File
    • Debug: 选择你的调试器(如ST-Link Debugger),并Settings中确认SWD接口识别到芯片ID。
    • Utilities: 同样配置下载器。

3.3 8051开发环境:Keil C51

过程类似,但编译器选择C51。芯片型号选择AT89C52STC89C52(需要手动添加STC的器件数据库)。

3.4 “点灯大师”的第一步:LED闪烁

原理: LED(发光二极管)阳极接GPIO,阴极接地(共阴极接法)。GPIO输出高电平(3.3V/5V),LED两端有压差而点亮;输出低电平(0V),LED熄灭。

示例1: STM32 HAL库实现LED闪烁 (GPIOA Pin5)
/* main.c */
#include "stm32f1xx_hal.h" // HAL库头文件

#define LED_PIN GPIO_PIN_5
#define LED_PORT GPIOA

void SystemClock_Config(void);
static void MX_GPIO_Init(void);

int main(void) {
  HAL_Init(); // 初始化HAL库
  SystemClock_Config(); // 配置系统时钟为72MHz
  MX_GPIO_Init(); // 初始化GPIO

  while (1) {
    HAL_GPIO_TogglePin(LED_PORT, LED_PIN); // 翻转LED引脚电平
    HAL_Delay(500); // 延迟500ms, HAL库提供的毫秒延迟函数
  }
}

void SystemClock_Config(void) {
  // 通常由CubeMX自动生成,此处简化
  RCC_OscInitTypeDef osc = {0};
  RCC_ClkInitTypeDef clk = {0};
  osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  osc.HSEState = RCC_HSE_ON;
  osc.HSIState = RCC_HSI_ON;
  osc.PLL.PLLState = RCC_PLL_ON;
  osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  osc.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz * 9 = 72MHz
  if (HAL_RCC_OscConfig(&osc) != HAL_OK) Error_Handler();
  clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
  clk.APB1CLKDivider = RCC_HCLK_DIV2;
  clk.APB2CLKDivider = RCC_HCLK_DIV1;
  if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2) != HAL_OK) Error_Handler();
}

static void MX_GPIO_Init(void) {
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA的时钟!非常重要!
  GPIO_InitStruct.Pin = LED_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出模式
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LED_PORT, &GPIO_InitStruct);
}

关键点

  1. __HAL_RCC_GPIOA_CLK_ENABLE(): 在STM32中,每个外设的时钟默认是关闭的以节能,使用前必须手动开启时钟。这是与8051的重大区别。
  2. GPIO_MODE_OUTPUT_PP: 推挽输出,能稳定输出高/低电平,驱动能力强。
  3. HAL_Delay(): 基于SysTick定时器实现的阻塞式延迟。
示例2: 8051直接寄存器操作实现LED闪烁 (P1.0)
/* main.c */
#include <reg52.h> // 包含8051特殊功能寄存器定义的头文件
#include <intrins.h> // 包含_nop_()等 intrinsics

sbit LED = P1^0; // 将P1口的第0位定义为LED

void delay_ms(unsigned int ms) {
    // 不精确的软件延时函数,受晶振和优化影响
    unsigned int i, j;
    for(i = 0; i < ms; i++)
        for(j = 0; j < 114; j++); // 针对11.0592MHz校准的循环次数
}

void main() {
    while(1) {
        LED = ~LED; // 位取反,翻转LED状态
        delay_ms(500); // 延时500ms
    }
}

关键点

  1. sbit: 8051特有的位寻址关键字,可以直接操作端口的某一位。
  2. P1: 是特殊功能寄存器(SFR),直接对应P1端口。
  3. 软件延时: 8051通常使用循环实现粗略的软件延时,精度差。高精度需求需使用定时器。

第四章:核心外设深度剖析与代码实战

4.1 通用输入输出 (GPIO)

GPIO是单片机与物理世界交互最基本的方式。除了简单的输出,它还能配置为输入、中断、复用功能等。

STM32 GPIO模式详解
// 1. 浮空输入 (GPIO_MODE_INPUT_FLOATING): 引脚电平完全由外部电路决定,默认状态不确定。
//    用于连接按键、开关等,外部必须有上拉或下拉电阻确定空闲状态。
// 2. 上拉/下拉输入 (GPIO_MODE_INPUT_PULLUP/PULLDOWN): 内部集成上拉/下拉电阻。
//    简化外围电路。例如按键一端接地,GPIO配置为上拉输入,按键未按下时为高电平,按下时为低电平。
// 3. 模拟输入 (GPIO_MODE_ANALOG): 引脚连接到ADC或比较器,关闭数字功能。
// 4. 推挽输出 (GPIO_MODE_OUTPUT_PP): 可以强输出高或低电平,驱动电流大(如点亮LED)。
// 5. 开漏输出 (GPIO_MODE_OUTPUT_OD): 只能拉低或高阻态。要输出高电平需外部上拉电阻。
//    常用于I2C等总线,实现“线与”功能。
// 6. 复用推挽/开漏 (GPIO_MODE_AF_PP/OD): 引脚作为外设(如USART_TX, SPI_SCK)的通道。

// 示例: 配置PA0为上拉输入,检测按键;配置PA1为开漏输出,控制LED(外部上拉)。
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 配置PA0为上拉输入
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置PA1为开漏输出
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL; // 开漏输出一般外部上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 读取按键状态
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
    // 按键被按下(因为上拉,按下接地为低电平)
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // 输出高阻态(靠外部上拉为高),LED灭
} else {
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // 输出低电平,LED亮
}

4.2 中断系统 (Interrupt)

中断是单片机实时响应外部或内部事件的核心机制。当事件发生时,CPU暂停当前任务,转去执行对应的中断服务函数 (ISR),执行完毕后返回原任务。

STM32外部中断 (EXTI) 示例:按键中断控制LED
/* main.c */
#include "stm32f1xx_hal.h"
#include "stm32f1xx_hal_gpio.h"

#define BUTTON_PIN GPIO_PIN_0
#define BUTTON_PORT GPIOA
#define BUTTON_EXTI_IRQn EXTI0_IRQn
#define LED_PIN GPIO_PIN_5
#define LED_PORT GPIOA

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_EXTI_Init(void);

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_EXTI_Init();

  while (1) {
    // 主循环可以处理其他任务,LED由中断控制
    HAL_Delay(1000);
  }
}

static void MX_GPIO_Init(void) {
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  __HAL_RCC_GPIOA_CLK_ENABLE();

  // 配置按键引脚为上拉输入
  GPIO_InitStruct.Pin = BUTTON_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发中断
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(BUTTON_PORT, &GPIO_InitStruct);

  // 配置LED引脚为输出
  GPIO_InitStruct.Pin = LED_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LED_PORT, &GPIO_InitStruct);

  // 设置EXTI0中断优先级并使能
  HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}

// EXTI0中断服务函数
void EXTI0_IRQHandler(void) {
  HAL_GPIO_EXTI_IRQHandler(BUTTON_PIN); // 处理中断标志位
}

// HAL库回调函数,在EXTI中断处理完成后被调用
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
  if(GPIO_Pin == BUTTON_PIN) {
    HAL_GPIO_TogglePin(LED_PORT, LED_PIN); // 翻转LED
    // 简单的按键消抖(实际应用需要更严谨的消抖,如定时器)
    HAL_Delay(50);
    while(HAL_GPIO_ReadPin(BUTTON_PORT, BUTTON_PIN) == GPIO_PIN_RESET); // 等待按键释放
    HAL_Delay(50);
  }
}
8051外部中断示例

8051有2个外部中断(INT0, INT1)。

#include <reg52.h>

sbit LED = P1^0;
sbit KEY = P3^2; // INT0对应P3.2

void ext0_isr() interrupt 0 { // interrupt 0 表示INT0的中断服务函数
    LED = ~LED;
    // 8051中断标志位硬件清除,但需要消抖和等待释放
    delay_ms(20);
    while(!KEY);
    delay_ms(20);
}

void main() {
    IT0 = 1; // 设置INT0为下降沿触发 (1: 边沿触发, 0: 低电平触发)
    EX0 = 1; // 使能INT0中断
    EA = 1;  // 开启全局中断开关

    while(1) {
        // 主程序
    }
}

4.3 定时器/计数器 (Timer)

定时器是单片机的“心脏”,用途极广:精确延时、PWM生成、输入捕获、输出比较等。

STM32通用定时器 (TIM) 示例:1ms定时中断 + PWM输出
/* 目标: 1. 使用TIM2产生1ms定时中断,累计毫秒数。 2. 使用TIM3的CH1 (PA6) 产生频率1kHz,占空比50%的PWM波。 */

#include "stm32f1xx_hal.h"

TIM_HandleTypeDef htim2;
TIM_HandleTypeDef htim3;

volatile uint32_t g_millis = 0; // 全局毫秒计数器

void SystemClock_Config(void);
static void MX_TIM2_Init(void); // 用于定时中断
static void MX_TIM3_Init(void); // 用于PWM输出

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_TIM2_Init();
  MX_TIM3_Init();

  HAL_TIM_Base_Start_IT(&htim2); // 启动TIM2并开启更新中断
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 启动TIM3的通道1 PWM输出

  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
  __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500); // 设置占空比 CCR1 = 500

  while (1) {
    // 利用g_millis实现非阻塞延时
    static uint32_t last_tick = 0;
    if(HAL_GetTick() - last_tick >= 1000) { // 每1000ms执行一次
        last_tick = HAL_GetTick();
        // 可以在这里执行定期任务,如打印g_millis
    }
    // 主循环其他任务
  }
}

// TIM2初始化: 1ms中断一次
static void MX_TIM2_Init(void) {
  __HAL_RCC_TIM2_CLK_ENABLE();
  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 7200 - 1; // 72MHz / 7200 = 10kHz
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 10 - 1; // 10kHz / 10 = 1kHz -> 1ms
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  HAL_TIM_Base_Init(&htim2);
  HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(TIM2_IRQn);
}

// TIM3 PWM初始化: 1kHz, 分辨率1us (ARR=999)
static void MX_TIM3_Init(void) {
  __HAL_RCC_TIM3_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  // 配置PA6为复用推挽输出(TIM3_CH1)
  GPIO_InitStruct.Pin = GPIO_PIN_6;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz -> 1us
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 1000 - 1; // 周期1000us -> 1kHz
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  HAL_TIM_PWM_Init(&htim3);

  TIM_OC_InitTypeDef sConfigOC = {0};
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 500; // 初始占空比500/1000 = 50%
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
}

// TIM2中断服务函数
void TIM2_IRQHandler(void) {
  HAL_TIM_IRQHandler(&htim2);
}
// TIM2更新中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
  if(htim->Instance == TIM2) {
    g_millis++;
  }
}

公式精讲

  • 定时器时钟CK_CNT = APB1_CLK / (PSC + 1)。APB1时钟在72MHz系统下为36MHz(因为APB1分频系数为2)。但STM32有一个倍频器,当APB1分频系数不为1时,定时器时钟会翻倍,所以CK_CNT = 36MHz * 2 = 72MHz
  • 计数周期T = (ARR + 1) / CK_CNT
  • 本例TIM2PSC=7199, ARR=9, 则 T = (9+1) / (72MHz / 7200) = 10 / 10kHz = 0.001s = 1ms
8051定时器示例:模式1,50ms中断
#include <reg52.h>

#define FOSC 11059200L // 晶振频率
#define T1MS (65536 - FOSC/12/1000) // 1ms定时器初值计算 (12T模式)

unsigned int ms_count = 0;

void timer0_isr() interrupt 1 { // 定时器0中断服务函数
    static unsigned int cnt = 0;
    TH0 = T1MS >> 8; // 重装初值
    TL0 = T1MS & 0xFF;
    if(++cnt >= 50) { // 50ms到
        cnt = 0;
        ms_count += 50;
        P1_0 = ~P1_0; // 每50ms翻转一次LED
    }
}

void main() {
    TMOD = 0x01; // 设置定时器0为模式1 (16位定时器)
    TH0 = T1MS >> 8;
    TL0 = T1MS & 0xFF;
    TR0 = 1; // 启动定时器0
    ET0 = 1; // 使能定时器0中断
    EA = 1;  // 开总中断

    while(1);
}

4.4 串口通信 (UART)

UART是异步串行通信,是最常用、最简单的设备间通信协议之一。

STM32 UART 示例:接收PC指令并回显 + 发送数据
#include "stm32f1xx_hal.h"
#include <string.h>
#include <stdio.h>

UART_HandleTypeDef huart1; // USART1
#define RX_BUFFER_SIZE 64
uint8_t rx_buffer[RX_BUFFER_SIZE];
uint8_t rx_data;
uint32_t rx_index = 0;

void SystemClock_Config(void);
static void MX_USART1_UART_Init(void);
static void MX_GPIO_Init(void);

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  // 启动串口空闲中断接收
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
  HAL_UART_Receive_IT(&huart1, &rx_data, 1); // 启动接收第一个字节

  printf("STM32 UART Demo Ready!\r\n"); // 使用printf重定向

  while (1) {
    // 主循环
  }
}

// USART1初始化: 115200波特率,8数据位,1停止位,无校验
static void MX_USART1_UART_Init(void) {
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  HAL_UART_Init(&huart1);
}

// 重定向printf到串口
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE {
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

// 串口接收中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if(huart->Instance == USART1) {
        if(rx_index < RX_BUFFER_SIZE - 1) {
            rx_buffer[rx_index++] = rx_data;
        }
        // 继续接收下一个字节
        HAL_UART_Receive_IT(&huart1, &rx_data, 1);
    }
}

// 串口空闲中断回调函数(需要自定义处理)
void USART1_IRQHandler(void) {
    HAL_UART_IRQHandler(&huart1);
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        // 空闲中断触发,表示一帧数据接收完成
        rx_buffer[rx_index] = '\0'; // 添加字符串结束符
        printf("Recv: %s\r\n", rx_buffer); // 回显接收到的数据

        // 简单的指令处理
        if(strcmp((char*)rx_buffer, "LED ON") == 0) {
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
            printf("LED Turned ON.\r\n");
        } else if(strcmp((char*)rx_buffer, "LED OFF") == 0) {
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
            printf("LED Turned OFF.\r\n");
        }

        rx_index = 0; // 清空缓冲区索引,准备接收下一帧
        memset(rx_buffer, 0, RX_BUFFER_SIZE);
    }
}
8051 UART 示例
#include <reg52.h>
#include <stdio.h> // 用于printf,但需要自己实现putchar

#define FOSC 11059200L
#define BAUD 9600
#define T1_RELOAD (256 - FOSC/12/32/BAUD) // 波特率发生器使用定时器1模式2

bit rx_flag = 0;
char rx_buf;

void uart_isr() interrupt 4 {
    if(RI) {
        RI = 0; // 清除接收中断标志
        rx_buf = SBUF; // 读取接收到的数据
        rx_flag = 1;   // 设置接收完成标志
        SBUF = rx_buf; // 回传(最简单的回显)
    }
    if(TI) {
        TI = 0; // 清除发送中断标志
        // 发送完成处理
    }
}

void uart_init() {
    SCON = 0x50; // 模式1,8位UART,允许接收
    TMOD &= 0x0F; // 清除定时器1模式位
    TMOD |= 0x20; // 定时器1,模式2(8位自动重装)
    TH1 = TL1 = T1_RELOAD; // 设置重载值
    TR1 = 1; // 启动定时器1(作为波特率发生器)
    ES = 1;  // 使能串口中断
    EA = 1;  // 开总中断
}

void main() {
    uart_init();
    printf("8051 UART Ready!\r\n"); // 注意:需要实现putchar函数发送单个字符
    while(1) {
        if(rx_flag) {
            rx_flag = 0;
            // 处理接收到的字符 rx_buf
            if(rx_buf == 'A') {
                P1 = 0xFF; // 点亮所有LED(假设共阴极)
            }
        }
    }
}

// 简单的putchar实现,用于printf
char putchar(char c) {
    SBUF = c;
    while(!TI); // 等待发送完成
    TI = 0;
    return c;
}

4.5 模数转换器 (ADC)

ADC将连续的模拟信号(如电压)转换为数字量,是连接传感器(温度、光强、压力)的关键。

STM32 ADC 示例:轮询模式读取电位器电压
#include "stm32f1xx_hal.h"
#include <stdio.h>

ADC_HandleTypeDef hadc1;

void SystemClock_Config(void);
static void MX_ADC1_Init(void);
static void MX_GPIO_Init(void);

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_ADC1_Init();

  HAL_ADC_Start(&hadc1); // 启动ADC转换

  printf("ADC Demo: Reading POT on PA1\r\n");
  while (1) {
    HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); // 等待转换完成
    uint16_t adc_value = HAL_ADC_GetValue(&hadc1); // 获取转换结果
    // STM32F103 ADC为12位,值范围0-4095
    float voltage = (float)adc_value / 4095.0f * 3.3f; // 计算电压值(假设参考电压为3.3V)
    printf("ADC: %4d, Voltage: %.3f V\r\n", adc_value, voltage);
    HAL_Delay(500);
  }
}

static void MX_ADC1_Init(void) {
  __HAL_RCC_ADC1_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  // 配置PA1为模拟输入
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin = GPIO_PIN_1;
  GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  hadc1.Instance = ADC1;
  hadc1.Init.ScanConvMode = DISABLE; // 单通道,不扫描
  hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换模式
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 数据右对齐
  hadc1.Init.NbrOfConversion = 1;
  HAL_ADC_Init(&hadc1);

  // 配置ADC通道,采样时间
  ADC_ChannelConfTypeDef sConfig = {0};
  sConfig.Channel = ADC_CHANNEL_1; // PA1对应ADC1的通道1
  sConfig.Rank = 1;
  sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES5;
  HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}

ADC关键参数

  • 分辨率: 12位表示有2^12=4096个等级。
  • 参考电压(Vref+): 决定了ADC的输入范围。STM32F103的Vref+通常连接到VDDA(3.3V)。测量电压范围是0~Vref+。
  • 采样时间: 需要足够的时间让外部信号对内部采样电容充电。信号源阻抗越高,需要的采样时间越长。

第五章:综合实战项目——智能温湿度监测站

项目目标: 使用STM32F103,通过DHT11温湿度传感器采集数据,在0.96寸OLED显示屏(I2C接口)上实时显示,并通过串口将数据发送到PC上位机。同时,通过按键可以切换显示模式(温度/湿度)。

5.1 硬件连接

  • STM32F103C8T6
  • DHT11: DATA -> PB0, VCC -> 3.3V, GND -> GND。
  • OLED (SSD1306, I2C): SCL -> PB6, SDA -> PB7, VCC -> 3.3V, GND -> GND。
  • 按键: KEY -> PB1(上拉输入,按下接地)。
  • 串口: USART1_TX(PA9) -> USB-TTL的RX, USART1_RX(PA10) -> USB-TTL的TX。

5.2 软件设计(核心代码框架)

由于代码较长,这里展示核心逻辑和模块化设计思路。完整工程需要整合DHT11驱动、OLED驱动、按键扫描和主逻辑。

/* main.c - 综合项目主文件 */
#include "stm32f1xx_hal.h"
#include "dht11.h"
#include "ssd1306.h"
#include "stdio.h"

// 全局变量
uint8_t g_display_mode = 0; // 0:温度,1:湿度
float g_temperature, g_humidity;

// 函数声明
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
static void MX_I2C1_Init(void);
void KEY_Scan(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    MX_I2C1_Init();

    // 外设初始化
    OLED_Init(); // 初始化OLED
    DHT11_Init(); // 初始化DHT11的GPIO

    OLED_ShowString(0, 0, "TempHum Station", 16);
    HAL_Delay(1000);
    OLED_Clear();

    printf("System Start...\r\n");

    while (1) {
        // 1. 按键扫描,切换模式
        KEY_Scan();

        // 2. 读取DHT11数据(注意:DHT11最小读取间隔为1s)
        if(DHT11_ReadData(&g_temperature, &g_humidity) == DHT11_OK) {
            // 3. 串口打印
            printf("Temp: %.1fC, Hum: %.1f%%\r\n", g_temperature, g_humidity);

            // 4. OLED显示
            OLED_Clear();
            if(g_display_mode == 0) {
                char buf[20];
                sprintf(buf, "Temperature:");
                OLED_ShowString(0, 0, buf, 16);
                sprintf(buf, "  %.1f C", g_temperature);
                OLED_ShowString(0, 2, buf, 16);
                OLED_ShowString(0, 4, "Mode: Temp", 16);
            } else {
                char buf[20];
                sprintf(buf, "Humidity:");
                OLED_ShowString(0, 0, buf, 16);
                sprintf(buf, "  %.1f %%", g_humidity);
                OLED_ShowString(0, 2, buf, 16);
                OLED_ShowString(0, 4, "Mode: Hum", 16);
            }
        } else {
            printf("DHT11 Read Failed!\r\n");
            OLED_ShowString(0, 0, "Sensor Error!", 16);
        }

        HAL_Delay(2000); // 每2秒读取一次
    }
}

// 简单的按键扫描函数(需消抖)
void KEY_Scan(void) {
    static uint8_t key_state = 1, last_state = 1;
    static uint32_t press_time = 0;

    key_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);
    if(key_state == 0 && last_state == 1) { // 检测到下降沿(按下)
        press_time = HAL_GetTick();
    }
    if(key_state == 1 && last_state == 0) { // 检测到上升沿(释放)
        if((HAL_GetTick() - press_time) > 50) { // 消抖,按下时间大于50ms
            g_display_mode = !g_display_mode; // 切换模式
            printf("Display Mode Switched to: %s\r\n", g_display_mode?"Humidity":"Temperature");
        }
    }
    last_state = key_state;
}

// ... 其他初始化函数(由CubeMX生成或手动编写)

项目要点

  1. 模块化: 将DHT11、OLED的驱动代码分别写在dht11.c/hssd1306.c/h中,提高代码可读性和复用性。
  2. 时序严格: DHT11是单总线协议,对时序要求非常严格,必须用微秒级延时(HAL_Delay_us)或精准的定时器来实现。
  3. I2C通信: OLED驱动使用HAL库的I2C函数进行读写。
  4. 状态管理: 使用全局变量g_display_mode来管理显示状态,按键负责切换。

第六章:进阶之路与资源推荐

6.1 进阶学习方向

  1. 深入内核: 理解ARM Cortex-M的NVIC(嵌套向量中断控制器)、SysTick、电源管理。
  2. 掌握DMA: 直接存储器访问,实现外设与内存间无需CPU干预的高速数据传输(如ADC连续采样、串口大数据传输)。
  3. 实时操作系统(RTOS): 学习FreeRTOS,掌握多任务、队列、信号量、互斥锁等概念,开发复杂应用。
  4. 通信协议: 深入研究SPI、I2C、CAN、USB等协议。
  5. 低功耗设计: 学习STM32的睡眠、停机、待机模式,应用于电池供电设备。
  6. 固件架构: 学习状态机、事件驱动、面向对象思想在C语言中的应用。

6.2 常用资源与工具

  • 官方资料
    • STM32CubeMX: 意法半导体官方图形化配置工具,生成初始化代码,必备神器
    • Datasheet: 芯片数据手册,查引脚、电气特性。
    • Reference Manual: 参考手册,详细描述所有外设的寄存器、功能,开发者圣经
  • 开发社区
    • ST CommunityARM Community
    • GitHub: 海量的开源项目和驱动库。
  • 硬件工具
    • 万用表、示波器、逻辑分析仪: 调试硬件问题的“三件套”。
    • ST-Link V2: 性价比最高的STM32下载调试器。

6.3 总结

单片机学习是一个理论与实践紧密结合的过程。切忌只看书、只看视频。一定要动手

  1. 买一块开发板(如STM32F103最小系统板)。
  2. 按照本文的示例,从点灯开始,一个一个模块地敲代码、调试、观察现象。
  3. 遇到问题,学会查阅手册、使用调试器、在论坛搜索。
  4. 尝试修改、组合代码,完成自己的小项目。

从点亮第一个LED的兴奋,到成功读取第一个传感器数据的成就感,再到最终完成一个综合项目的满足感,这正是嵌入式开发的魅力所在。希望这篇超过万字的指南,能成为你单片机学习道路上的一盏明灯,助你从零到一,进而迈向更广阔的嵌入式世界。

路虽远,行则将至;事虽难,做则必成。共勉!


Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐