单片机知识框架
本文介绍了单片机开发的核心概念与关键技术。单片机是一种集成CPU、存储器和I/O接口的微型计算机芯片,通过编程实现自动化控制。文章重点阐述了C语言操作内存的机制,包括变量定义、指针使用和内存访问方法。针对STM32开发,详细分析了AHB总线的双桥设计原理,指出其通过高低速外设分离实现性能优化和功耗控制。同时强调了volatile关键字在硬件寄存器访问和中断处理中的关键作用,并提供了使用范例。最后简
一、单片机的定义与核心概念
单片机(Microcontroller Unit, MCU)是一种集成微型计算机系统核心功能的芯片,包含中央处理器(CPU)、存储器(RAM/ROM)、输入/输出接口(I/O)及定时器/计数器等模块。其特点是通过编程控制外部设备,实现自动化操作。






****

单片机开发的本质:操作内存
- 为了实现某一具体的功能,我们因该操作哪块内存
- 如何编写C语言操作内存
二、C语言是怎样操作内存的


C语言中既有变量又可以操作内存空间---指针
//假设现在有10块内存
int a;//在这10块内存中,要一个空闲内存,将a这个符号和这块内存绑定
a = 1;//把数值1存入(写入)a这块内存中
int b;
b = a;//把a内存中的值,复制到b内存中
//如果想通过c这变量去访问任意一块内存
//那么就不能用int c来定义一个普遍变量
//而要用 int* c来定义一个变量
//被int* 定义的变量,就可以通过这个变量访问任意一块内存
int* c;
c = 10;
//如何通过c这个指针访问这个第10块内存呢
*c = 2;


//向14号内存中,写入5
int* d;
d = 14;
*d = 5;

int* p;
p = 0x0C;//在代码中,16进制用0x开头,后面写0123456789ABCDEF





short a;//把两块内存,合并成一块使用
a = 10;

int b;//int是把4个字节合并在一起使用
b = 1;
char c;//单个字节独立使用
c = 2;

第二种方法,把两块内存,合成一个,一次赋值




三、为了完成那块功能,操作那块内存










四、实验步骤
4.1建立工程












4.2使用仿真环境











4.3优化代码
为什么要优化代码
为了实现代码的可读性,可维护性,可移植性








思考:
stm32 AHB系统总线为什么有两个桥
STM32的AHB系统有两个桥是为了实现性能、功耗和成本的完美平衡。
让我们深入解析:
核心答案:为什么是两个?
STM32中常见的“两个桥”指的是连接AHB系统总线到两条不同APB(Advanced Peripheral Bus)总线的桥接器:
-
AHB 到 APB1 的桥
-
AHB 到 APB2 的桥
这并非随意的设计,而是基于以下几个关键原因:
1. 外设速度需求不同:高低速分离
这是最直接的原因。STM32的外设性能差异巨大:
-
高速外设:如GPIO(翻转引脚)、高级定时器(TIM1)、SPI1、ADC等,需要很高的时钟频率才能发挥性能。
-
低速外设:如I2C、UART、看门狗、基本定时器等,工作在较低频率下就完全足够。
解决方案:
-
将高速外设全部挂在 APB2 总线上。
-
将低速外设全部挂在 APB1 总线上。
好处:这样可以为两条总线设置不同的时钟频率,实现精细化的时钟管理。
2. 功耗优化
功耗是嵌入式系统的核心指标。
-
如果所有外设都挂在一条总线上,那么这条总线必须运行在最高速度(以满足高速外设),即使你只在使用一个低速的I2C,整个总线也在高速运行,浪费功耗。
-
通过两个桥接分离后:
-
当CPU只访问APB1上的低速外设(如UART)时,APB2总线可以保持低频甚至关闭时钟,大大节省功耗。
-
反之亦然。
-
这实现了“按需分配时钟”,是低功耗设计的关键。
3. 减少总线负载,提高性能
如果把所有外设都直接挂在AHB或一条APB总线上,总线的电容负载会很重,这会导致信号完整性变差,限制最高运行频率。
通过两个桥接,相当于把外设负载分散到了两条独立的通道上:
-
APB1桥 承载所有低速外设的负载。
-
APB2桥 承载所有高速外设的负载。
这样每条总线(AHB, APB1, APB2)的负载都更轻,时序更容易满足,可以稳定地在更高频率下运行。
4. 系统结构与成本优化
从芯片设计角度看:
-
APB总线协议非常简单,对应的APB桥接逻辑也比AHB简单得多。
-
使用两个简单的APB桥,比在AHB上增加复杂的仲裁和多主支持来管理所有外设,成本更低,设计更简单。
-
这符合ARM AMBA架构的初衷:用简单的APB总线来连接大量低带宽的外设,而让高性能的AHB专注于CPU、DMA和内存等核心单元。
实际例子:以STM32F103(Cortex-M3)时钟树为例
在这个经典系列中:
-
APB2总线:最大频率 72 MHz。
-
外设:GPIOA-G, SPI1, ADC1/2, TIM1, USART1等。
-
这些外设都需要高性能。
-
-
APB1总线:最大频率 36 MHz(通常是APB2的一半)。
-
外设:I2C1/2, SPI2/3, USART2/3/4/5, TIM2-TIM7等。
-
这些外设在36MHz下已完全够用。
-
如果只有一个APB桥,那么所有外设要么都只能跑到36MHz(限制了GPIO、ADC的性能),要么都跑到72MHz(对I2C来说过于浪费且增加功耗)。两个桥的设计完美解决了这个矛盾。
总结:一个精妙的“交通系统”比喻
您可以把STM32的内部总线想象成一个城市的交通网:
-
CPU核心:是 市中心。
-
AHB总线:是 城市环城高速,速度快,承载核心车流(数据流)。
-
APB2桥和APB2总线:是连接高速和高新区/商业区的快速路,车道宽、限速高(72MHz),服务高档写字楼(GPIO、高级定时器)。
-
APB1桥和APB1总线:是连接高速和老城区/住宅区的普通主干道,车道窄、限速低(36MHz),服务学校和社区中心(UART, I2C)。
为什么要有两条出高速的路(两个桥)?
因为如果所有车辆(数据)都挤在同一条路上去往所有区域,那么:
-
去高新区的跑不快(性能瓶颈)。
-
去老城区的车也造成了不必要的快速路拥堵(功耗浪费)。
-
整个交通系统难以管理(设计复杂)。
因此,STM32使用两个桥接,是一个在性能、功耗和成本之间取得的完美平衡的设计,是嵌入式系统架构智慧的体现。











volatile 关键字的使用
// 硬件寄存器必须用volatile
#define GPIOA_DATA *(volatile uint32_t *)0x40020000
void set_led(void) {
GPIOA_DATA = 0x01; // 写入硬件寄存器
// 编译器不能优化掉这个写入操作
}
// 没有 volatile - 可能被错误优化
int flag = 0;
void wait_for_flag(void) {
while (flag == 0) {
// 空循环等待flag改变
}
}
// 编译器可能认为flag永远不会改变,将循环优化成死循环
// 使用 volatile - 保证每次都会读取内存中的实际值
volatile int flag = 0;
void wait_for_flag(void) {
while (flag == 0) {
// 每次循环都会从内存读取flag的实际值
}
}
volatile关键字至关重要,它直接关系到程序的正确性和可靠性。让我详细解释其使用场景和重要性。
什么是volatile?
volatile是一个类型修饰符,告诉编译器:
-
该变量的值可能在任何时候被意外改变
-
禁止编译器对该变量的访问进行优化
-
每次访问都必须从内存中读取/写入,不能使用缓存值
在STM32中的关键应用场景
1. 硬件寄存器访问
这是最常见的应用场景。所有外设寄存器都必须声明为volatile。
// 正确的寄存器定义
#define GPIOA_MODER (*(volatile uint32_t *)0x40020000)
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014)
// 使用示例
void LED_Init(void) {
GPIOA_MODER |= (1 << 10); // 设置PA5为输出模式
}
void LED_Toggle(void) {
GPIOA_ODR ^= (1 << 5); // 翻转PA5状态
}
为什么必须用volatile?
编译器可能会认为连续写入同一个寄存器是冗余操作而进行优化,但实际上每次写入都会改变硬件状态。
2. 中断服务程序(ISR)中修改的全局变量
// 全局变量声明
volatile uint32_t systick_count = 0;
volatile uint8_t button_pressed = 0;
// SysTick中断服务函数
void SysTick_Handler(void) {
systick_count++; // 在中断中修改变量
}
// 外部中断服务函数
void EXTI0_IRQHandler(void) {
button_pressed = 1;
EXTI->PR |= EXTI_PR_PR0; // 清除中断标志
}
// 主循环中使用
int main(void) {
SysTick_Config(SystemCoreClock / 1000); // 1ms中断
while(1) {
if(button_pressed) {
// 处理按键
button_pressed = 0;
}
// 基于系统滴答计数的延时
uint32_t start_time = systick_count;
while((systick_count - start_time) < 1000) {
// 等待1秒
}
}
}
3. 多线程共享变量(在RTOS中)
// 任务间通信的共享变量
volatile uint8_t message_ready = 0;
volatile uint32_t shared_data = 0;
// 生产者任务
void ProducerTask(void *pvParameters) {
while(1) {
shared_data = read_sensor_data();
message_ready = 1; // 通知消费者
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 消费者任务
void ConsumerTask(void *pvParameters) {
while(1) {
if(message_ready) {
process_data(shared_data);
message_ready = 0;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
4. 状态标志和通信标志
// DMA传输状态
volatile uint8_t dma_transfer_complete = 0;
void DMA1_Channel1_IRQHandler(void) {
if(DMA1->ISR & DMA_ISR_TCIF1) {
dma_transfer_complete = 1;
DMA1->IFCR |= DMA_IFCR_CTCIF1; // 清除传输完成标志
}
}
// 等待DMA传输完成
void wait_for_dma_complete(void) {
while(!dma_transfer_complete) {
// 空循环等待 - 没有volatile可能会被优化掉!
}
dma_transfer_complete = 0;
}
不使用volatile的危险示例
// 错误示例 - 没有volatile
uint32_t sensor_value;
void ADC_IRQHandler(void) {
sensor_value = ADC1->DR; // 读取ADC值
}
int main(void) {
ADC_Init();
while(1) {
// 编译器可能优化为只读取一次sensor_value
// 导致程序永远使用第一个ADC采样值!
if(sensor_value > 1000) {
take_action();
}
}
}
volatile的使用规则和最佳实践
1. 正确的位置
// 正确
volatile uint32_t *ptr = (volatile uint32_t *)0x40021000;
// 指针指向的内容是volatile,但指针本身不是
volatile uint32_t * const p_reg = (volatile uint32_t *)0x40021000;
// 指针本身也是volatile(很少需要)
uint32_t * volatile p_var;
2. 结构体中的volatile
typedef struct {
volatile uint32_t CR; // 控制寄存器
volatile uint32_t SR; // 状态寄存器
volatile uint32_t DR; // 数据寄存器
} USART_TypeDef;
#define USART1 ((USART_TypeDef *)0x40011000)
void USART_SendChar(char c) {
while(!(USART1->SR & USART_SR_TXE)); // 等待发送缓冲区空
USART1->DR = c;
}
3. 与const结合使用
// 只读的硬件寄存器
const volatile uint32_t *device_id = (const volatile uint32_t *)0x1FFFF7E8;
// 在中断中修改但主程序只读的变量
volatile const uint32_t *get_system_time_ptr(void) {
return &systick_count; // 返回指向volatile变量的指针
}
常见误区
1. volatile不能保证原子性
// 错误:volatile不保证原子操作
volatile uint32_t counter = 0;
void increment_counter(void) {
counter++; // 这不是原子操作!
}
// 正确:需要额外的保护机制
void safe_increment_counter(void) {
__disable_irq(); // 禁用中断
counter++;
__enable_irq(); // 启用中断
}
2. volatile不能替代内存屏障
// 在DMA操作中可能需要内存屏障
void start_dma_transfer(void *src, void *dest, uint32_t size) {
memcpy(dma_buffer, src, size); // 准备数据
// 确保数据完全写入内存
__DSB();
// 现在可以安全启动DMA
DMA1->CCR |= DMA_CCR_EN;
}
总结;
在STM32开发中,必须使用volatile的情况:
-
所有内存映射的外设寄存器
-
在中断服务程序中修改的全局变量
-
RTOS中任务间共享的变量
-
状态标志和通信标志
-
被DMA修改的内存区域
记住这个简单规则:如果一个变量可能在你不知道的情况下被改变,就应该声明为volatile。正确使用volatile是确保STM32程序稳定可靠运行的基础












五、中断





5.1GPIO中断演示














5.2系统结构讲解***








继电器工作原理


源于老师上课讲解笔记整合
昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链
更多推荐



所有评论(0)