从零到一:单片机基础深度解析与实战指南(超详细)
摘要:本文系统介绍了单片机的基础知识与应用开发,涵盖主流架构(8051和ARM Cortex-M)的核心原理与实战方法。从单片机内部结构、最小系统搭建到开发环境配置,详细解析了GPIO、定时器、通信接口等关键模块。文章以STM32F103为例,提供完整的硬件设计指南和软件开发流程,并对比了不同单片机架构的特点,为初学者构建了从理论到实践的完整学习路径。通过丰富的代码示例和原理图说明,帮助读者快速掌
从零到一:单片机基础深度解析与实战指南
摘要: 单片机(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 主流单片机架构与选型
-
基于Intel MCS-51的8位单片机:
- 代表: AT89C51/S51, STC89C52RC。经典、教学首选。
- 特点: 架构简单,资料丰富,易于理解单片机原理,适合入门。
-
ARM Cortex-M系列32位单片机:
- 代表: STM32(意法半导体), GD32(兆易创新), Nordic nRF52(低功耗蓝牙)。
- 特点: 性能强大(主频从几十MHz到几百MHz),外设丰富,生态完善,是当前工业界和消费电子的绝对主流。本文将以STM32F103C8T6(又称”Blue Pill”)为例。
-
其他架构:
- AVR: Atmel公司产品,Arduino Uno的核心(ATmega328P),创客圈流行。
- PIC: Microchip公司产品,在工业控制、汽车电子中应用广泛。
- RISC-V: 开源指令集架构,新兴势力,未来可期。
初学者建议路线: 为了深刻理解原理,可以从8051开始;为了紧跟技术潮流并投入实用,应迅速转向ARM Cortex-M。
第二章:深入单片机内部:硬件结构与最小系统
2.1 单片机内部核心框图
以STM32F103为例,其内部结构复杂但高度模块化。理解下图是理解其所有功能的基础:
关键组件:
- 总线矩阵: 协调CPU、DMA、各外设对存储器和外设的访问,是高速数据流通的“交通枢纽”。
- 存储器: Flash存放代码,SRAM存放运行时的堆、栈、全局变量等。
- 外设: 挂在APB总线上的各种功能模块,是我们编程控制的主要对象。
2.2 搭建最小系统
要让一块“裸”的单片机跑起来,必须为其提供最基本的“生存条件”,这就是最小系统。
STM32F103C8T6最小系统原理图(关键部分):
解读:
- 电源(3.3V): 大多数STM32引脚耐压5V,但核心电压是3.3V,务必使用3.3V稳压器供电(如AMS1117-3.3)。VDDA和VSSA是模拟部分的电源,通常接一个滤波电感或磁珠后与数字电源相连。
- 复位电路: 由电阻、电容和按键组成。上电时,电容充电使NRST引脚保持一段时间低电平,实现上电复位。按键提供手动复位。
- 晶振电路: 外部8MHz晶振(配合两个负载电容)为系统提供精准时钟源。STM32内部有PLL(锁相环)可以将其倍频至72MHz(STM32F103的最大系统时钟)。
- 启动模式(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 开发流程与工具链
单片机开发遵循标准的嵌入式开发流程:
- 编写代码: 在PC上使用编辑器或IDE(集成开发环境)编写C/C++/汇编代码。
- 编译/链接: 使用交叉编译器将源代码转换成单片机可执行的机器码(Hex/Bin文件)。
- 下载/调试: 通过下载器/调试器(如ST-Link, J-Link, USB-TTL)将机器码烧录到单片机的Flash中,并可进行单步调试。
3.2 ARM (STM32) 开发环境:Keil MDK
以Keil MDK(ARM版)为例,这是最流行的商业IDE之一。
项目创建与配置步骤(精简):
Project -> New uVision Project,选择芯片型号STM32F103C8。- 管理运行时环境(RTE),添加
Device -> Startup和Device -> StdPeriph Drivers(如果使用标准库)。 - 配置魔术棒(Options for Target):
Target: 晶振频率设为8.0。Output: 勾选Create HEX File。Debug: 选择你的调试器(如ST-Link Debugger),并Settings中确认SWD接口识别到芯片ID。Utilities: 同样配置下载器。
3.3 8051开发环境:Keil C51
过程类似,但编译器选择C51。芯片型号选择AT89C52或STC89C52(需要手动添加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);
}
关键点:
__HAL_RCC_GPIOA_CLK_ENABLE(): 在STM32中,每个外设的时钟默认是关闭的以节能,使用前必须手动开启时钟。这是与8051的重大区别。GPIO_MODE_OUTPUT_PP: 推挽输出,能稳定输出高/低电平,驱动能力强。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
}
}
关键点:
sbit: 8051特有的位寻址关键字,可以直接操作端口的某一位。P1: 是特殊功能寄存器(SFR),直接对应P1端口。- 软件延时: 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。 - 本例TIM2:
PSC=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生成或手动编写)
项目要点:
- 模块化: 将DHT11、OLED的驱动代码分别写在
dht11.c/h和ssd1306.c/h中,提高代码可读性和复用性。 - 时序严格: DHT11是单总线协议,对时序要求非常严格,必须用微秒级延时(
HAL_Delay_us)或精准的定时器来实现。 - I2C通信: OLED驱动使用HAL库的I2C函数进行读写。
- 状态管理: 使用全局变量
g_display_mode来管理显示状态,按键负责切换。
第六章:进阶之路与资源推荐
6.1 进阶学习方向
- 深入内核: 理解ARM Cortex-M的NVIC(嵌套向量中断控制器)、SysTick、电源管理。
- 掌握DMA: 直接存储器访问,实现外设与内存间无需CPU干预的高速数据传输(如ADC连续采样、串口大数据传输)。
- 实时操作系统(RTOS): 学习FreeRTOS,掌握多任务、队列、信号量、互斥锁等概念,开发复杂应用。
- 通信协议: 深入研究SPI、I2C、CAN、USB等协议。
- 低功耗设计: 学习STM32的睡眠、停机、待机模式,应用于电池供电设备。
- 固件架构: 学习状态机、事件驱动、面向对象思想在C语言中的应用。
6.2 常用资源与工具
- 官方资料:
- STM32CubeMX: 意法半导体官方图形化配置工具,生成初始化代码,必备神器。
- Datasheet: 芯片数据手册,查引脚、电气特性。
- Reference Manual: 参考手册,详细描述所有外设的寄存器、功能,开发者圣经。
- 开发社区:
- ST Community, ARM Community。
- GitHub: 海量的开源项目和驱动库。
- 硬件工具:
- 万用表、示波器、逻辑分析仪: 调试硬件问题的“三件套”。
- ST-Link V2: 性价比最高的STM32下载调试器。
6.3 总结
单片机学习是一个理论与实践紧密结合的过程。切忌只看书、只看视频。一定要动手:
- 买一块开发板(如STM32F103最小系统板)。
- 按照本文的示例,从点灯开始,一个一个模块地敲代码、调试、观察现象。
- 遇到问题,学会查阅手册、使用调试器、在论坛搜索。
- 尝试修改、组合代码,完成自己的小项目。
从点亮第一个LED的兴奋,到成功读取第一个传感器数据的成就感,再到最终完成一个综合项目的满足感,这正是嵌入式开发的魅力所在。希望这篇超过万字的指南,能成为你单片机学习道路上的一盏明灯,助你从零到一,进而迈向更广阔的嵌入式世界。
路虽远,行则将至;事虽难,做则必成。共勉!
更多推荐
所有评论(0)