一、单片机的定义与核心概念

单片机(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)总线的桥接器:

  1. AHB 到 APB1 的桥

  2. 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)。

为什么要有两条出高速的路(两个桥)?
因为如果所有车辆(数据)都挤在同一条路上去往所有区域,那么:

  1. 去高新区的跑不快(性能瓶颈)。

  2. 去老城区的车也造成了不必要的快速路拥堵(功耗浪费)。

  3. 整个交通系统难以管理(设计复杂)。

因此,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的情况:

  1. 所有内存映射的外设寄存器

  2. 在中断服务程序中修改的全局变量

  3. RTOS中任务间共享的变量

  4. 状态标志和通信标志

  5. 被DMA修改的内存区域

记住这个简单规则:如果一个变量可能在你不知道的情况下被改变,就应该声明为volatile。正确使用volatile是确保STM32程序稳定可靠运行的基础

五、中断

5.1GPIO中断演示

 5.2系统结构讲解***

继电器工作原理

源于老师上课讲解笔记整合

Logo

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

更多推荐