一.C中的内存划分

在大多数单片机系统中,程序运行时有两类物理存储器:

  • FLASH(闪存)(非易失):存储代码和只读数据,掉电不丢失。

  • RAM(随机存取存储器)(易失):存储可读写的变量和堆栈,掉电丢失。


内存通常分为ROM(read only memory)和RAM (random access memory)。

其中,栈区、堆区以及静态区(ZI 段/RW段)是存在于RAM中的;

常量区和代码区存在于ROM中。


Data(数据)段:

包括RW段与BSS段(ZI段),BSS = ZI。

RW段存放的是已初始化的全局变量和静态变量;

BSS段存放的是未初始化的全局变量和静态变量。

Data段也可称为静态区(全局区)。

RO段:

代表只读段,存放的是常量数据, 如const修饰的全局变量。

Code段:

存放程序的可执行代码。

堆区:

用于动态分配内存,如malloc函数 所分配的内存存储在堆区;由程序员手动管理内存分配和释放。

栈区:

存放局部变量,形参,返回地址等;由系统自动分配内存和释放内存。


二.STM32系统架构图

STM32 主系统主要由四个驱动单元和四个被动单元构成。

四个驱动单元是:

内核DCode总线;   系统总线;   通用DMA1;   通用DMA2;

四个被动单元是:

AHB到APB的桥;  连接所有的APB设备;   内部FlASH闪存;   内部SRAM;   FSMC;

① ICode总线:该总线将M3内核指令总线和闪存指令接口相连,指令的预取在该总线上 面完成。

② DCode总线:该总线将M3内核的DCode总线与闪存存储器的数据接口相连接,常量 加载和调试访问在该总线上面完成。

③ 系统总线:该总线连接M3内核的系统总线到总线矩阵,总线矩阵协调内核和DMA间 访问。

④ DMA总线:该总线将DMA的AHB主控接口与总线矩阵相连,总线矩阵协调CPU的 DCode 和DMA到SRAM,闪存和外设的访问。

⑤ 总线矩阵:总线矩阵协调内核系统总线和 DMA 主控总线之间的访问仲裁,仲裁利用 轮换算法。

⑥ AHB/APB桥:这两个桥在AHB和2个APB总线间提供同步连接,APB1操作速度限于 36MHz,APB2 操作速度全速。


三.STM32 时钟系统

在STM32中,有五个时钟源,为HSI、HSE、LSI、LSE、PLL。

从时钟频率来分可以分为 高速时钟源和低速时钟源。

在这5个中HIS,HSE以及PLL是高速时钟,LSI和LSE是低速时钟。

从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中HSE和LSE是外部时钟源,其他的是内部时钟源。

下面我们看看STM32的5个时钟源,我们讲解顺序是按图中红圈标示的顺序:

①、HSI是高速内部时钟,RC振荡器,频率为8MHz。

②、HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为 4MHz~16MHz。我们的开发板接的是8M的晶振。

③、LSI是低速内部时钟,RC振荡器,频率为40kHz。独立看门狗的时钟源只能是LSI,同 时LSI还可以作为RTC的时钟源。

④、LSE是低速外部时钟,接频率为32.768kHz的石英晶体。这个主要是RTC的时钟源。

⑤、PLL 为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为 118
2~16 倍,但是其输出频率最大不得超过72MHz。

上面我们简要概括了STM32 的时钟源,那么这5个时钟源是怎么给各个外设以及系统提供时钟的呢? 图中我们用A ~E标示我们要讲解的地方。

A. B. C. D. E. MCO 是STM32 的一个时钟输出IO(PA8),它可以选择一个时钟信号输出,可以 选择为PLL输出的2分频、HSI、HSE、或者系统时钟。这个时钟可以用来给外 部其他系统提供时钟源。 这里是RTC时钟源,从图上可以看出,RTC的时钟源可以选择LSI,LSE,以及 HSE 的128分频。 从图中可以看出C处USB的时钟是来自PLL时钟源。

STM32中有一个全速功能 的USB模块,其串行接口引擎需要一个频率为48MHz的时钟源。该时钟源只能 从PLL输出端获取,可以选择为1.5分频或者1分频,也就是,当需要使用USB 模块时,PLL必须使能,并且时钟频率配置为48MHz或72MHz。 D处就是STM32的系统时钟SYSCLK,它是供STM32中绝大部分部件工作的时 钟源。系统时钟可选择为PLL输出、HSI或者HSE。系统时钟最大频率为72MHz, 当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。

这里的E处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最 终来源都是SYSCLK。SYSCLK 通过AHB分频器分频后送给各模块使用。

这些 模块包括:

①、AHB总线、内核、内存和DMA使用的HCLK时钟。

②、通过8分频后送给Cortex的系统定时器时钟,也就是systick了。

③、直接送给Cortex的空闲运行时钟FCLK。

④、送给APB1分频器。APB1分频器输出一路供APB1外设使用(PCLK1,最大 频率36MHz),另一路送给定时器(Timer)2、3、4倍频器使用。

⑤、送给APB2分频器。APB2分频器分频输出一路供APB2外设使用(PCLK2, 最大频率72MHz),另一路送给定时器(Timer)1倍频器使用。

其中需要理解的是APB1和APB2的区别,APB1上面连接的是低速外设,包括电源接口、 备份接口、CAN、USB、I2C1、I2C2、UART2、UART3等等,APB2上面连接的是高速外设包 括UART1、SPI1、Timer1、ADC1、ADC2、所有普通IO口(PA~PE)、第二功能IO口等。

APB2下面所挂的外设的时 钟要比APB1的高。


这里总结一下SystemInit()函数中设置的系统时钟大小:

SYSCLK(系统时钟) =72MHz

AHB总线时钟(使用SYSCLK) =72MHz

APB1总线时钟(PCLK1) =36MHz

APB2总线时钟(PCLK2) =72MHz

PLL时钟 =72MHz

以上关于时钟内容看的不是很明白,之后再来继续学习。

四.MDK下的C基础

1. 位操作

C 语言支持如下6中位操作

1) 不改变其他位的值的状况下,对某几个位进行设值。

这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作, 然后用|操作符设值。比如我要改变GPIOA的状态,可以先对寄存器的值进行&清零操作

GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0 然后再与需要设置的值进行|或运算

GPIOA->CRL|=0X00000040; //设置相应位的值,不改变其他位的值

2) 移位操作提高代码的可读性。

移位操作在单片机开发中也非常重要,下面让我们看看固件库的GPIO初始化的函数里 面的一行代码

GPIOx->BSRR = (((uint32_t)0x01) BSRR =0x0030;

这样的代码就不好看也不好重用了。 类似这样的代码很多:

GPIOA->ODR|=1<<5; //PA.5 输出高,不改变其他位

这样我们一目了然,5告诉我们是第5位也就是第6个端口,1告诉我们是设置为1了。

3) ~取反操作使用技巧 SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为0,同时 其他位都保留为1,简单的作法是直接给寄存器设置一个值:

TIMx->SR=0xFFF7;

这样的作法设置第3位为0,但是这样的作法同样不好看,并且可读性很差。看看库函数 代码中怎样使用的:

TIMx->SR = (uint16_t)~TIM_FLAG;

而TIM_FLAG 是通过宏定义定义的值:

#define TIM_FLAG_Update ((uint16_t)0x0001)

#define TIM_FLAG_CC1 ((uint16_t)0x0002)

看这个应该很容易明白,可以直接从宏定义中看出TIM_FLAG_Update就是设置的第0位了, 可读性非常强。

2.define 宏定义

define 是C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供 方便。常见的格式: #define 标识符 字符串 “标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。

例如: #define SYSCLK_FREQ_72MHz 72000000 定义标识符SYSCLK_FREQ_72MHz的值为72000000。

3. ifdef 条件编译

单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而 当条件不满足时则编译另一组语句。

条件编译命令最常见的形式为:

#ifdef 标识符

程序段1

#else 程序段2

#endif

它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译, 否则编译程序段2。 其中#else部分也可以没有,即:

#ifdef

程序段1

#endif

五.SYSTEM文件夹介绍

1.delay 文件夹代码介绍;

delay 文件夹内包含了delay.c和delay.h 两个文件,这两个文件用来实现系统的延时功能, 其中包含7个函数:

void delay_osschedlock(void);

void delay_osschedunlock(void);

void delay_ostimedly(u32 ticks);

void SysTick_Handler(void);

void delay_init(void);

void delay_ms(u16 nms);

void delay_us(u32 nus);

前面4个函数,仅在支持操作系统(OS)的时候,需要用到,而后面三个函数,则不论是 否支持OS都需要用到。 在介绍这些函数之前,我们先了解一下delay延时的编程思想:CM3内核的处理器,内部 包含了一个 SysTick 定时器,SysTick 是一个 24 位的倒计数定时器,当计数到 0 时,将从 RELOAD 寄存器中自动重装载定时初值,开始新一轮计数。只要不把它在 SysTick 控制及状 态寄存器中的使能位清除,就永不停息。就是利用STM32的内部SysTick来实现延时的,这样既不占用中断,也不占用系统定时器。

2.sys 文件夹代码介绍;

sys 文件夹内包含了sys.c和sys.h两个文件。在sys.h里面定义了STM32的IO口输入读取 宏定义和输出宏定义。sys.c里面只定义了一个中断分组函数。

IO 口的位操作实现

该部分代码在sys.h文件中,实现对STM32各个IO口的位操作,包括读入和输出。当然 在这些函数调用之前,必须先进行IO口时钟的使能和IO口功能定义。

此部分仅仅对IO口进 行输入输出读取和控制。 位带操作简单的说,就是把每个比特膨胀为一个32位的字,当访问这些字的时候就达到了 访问比特的目的,比如说BSRR寄存器有32个位,那么可以映射到32个地址上,我们去访问 这32个地址就达到访问32个比特的目的。这样我们往某个地址写1就达到往对应比特位写1 的目的,同样往某个地址写0就达到往对应的比特位写0的目的。

3.usart 文件夹代码介绍;

usart 文件夹内包含了 usart.c 和 usart.h 两个文件。这两个文件用于串口的初始化和中断接 收。这里只是针对串口1,比如你要用串口2或者其他的串口,只要对代码稍作修改就可以了。 usart.c里面包含了2个函数一个是void USART1_IRQHandler(void);另外一个是void uart_init(u32 bound);里面还有一段对串口 printf 的支持代码,如果去掉,则会导致 printf 无法使用,虽然软 件编译不会报错,但是硬件上STM32是无法启动的,这段代码不要去修改。


                                          关于上面这三个文件夹略写,后面学到了再去补充。


实验一:跑马灯实验

1.工程结构说明

📁 Start(启动与核心层)

存放芯片启动文件和底层核心代码:

文件 作用
startup_stm32f10x_md.s 启动文件(汇编),定义中断向量表、堆栈初始化、程序入口
core_cm3.c/h Cortex-M3 内核接口,提供 NVIC、SysTick 等内核级功能访问
stm32f10x.h 寄存器头文件,定义所有外设寄存器的地址和结构体
system_stm32f10x.c/h 系统初始化,配置时钟树(SYSCLK、AHB、APB)、PLL 等

📁 Library(标准外设库)

通常存放 ST 官方提供的 标准外设库(Standard Peripheral Library) 源文件,如:

  • stm32f10x_gpio.c
  • stm32f10x_usart.c
  • stm32f10x_rcc.c
  • ...

截图中该文件夹是折叠状态,展开后会有各个外设的驱动源文件。

📁 System(系统工具层)

用户自定义的系统级功能模块:

文件 作用
Delay.c/h 延时函数,实现微秒/毫秒级延时(通常基于 SysTick 定时器)

📁 User(用户应用层)

存放用户编写的应用代码:

文件 作用
main.c 主程序入口,包含 main() 函数和用户逻辑
stm32f10x_conf.h 库配置文件,选择需要哪些外设库头文件(通过 #include 控制)
stm32f10x_it.c/h 中断服务程序,存放所有外设的中断处理函数(如 USART1_IRQHandler、TIM2_IRQHandler 等)

2. 硬件设计

3. 软件设计

跑马灯实验我们主要用到的固件库文件是:


stm32f10x_gpio.c /stm32f10x_gpio.h stm32f10x_rcc.c/
stm32f10x_rcc.h misc.c/ misc.h stm32f10x_usart /
stm32f10x_usart.h


其中stm32f10x_rcc.h 头文件在每个实验中都要引入,因为系统时钟配置函数以及相关的外设时 钟使能函数都在这个其源文件 stm32f10x_rcc.c 中。stm32f10x_usart.h 和 misc.h 头文件在我们 SYSTEM文件夹中都需要使用到,所以每个实验都会引用。

实验代码如下:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
    /*开启时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);    //开启GPIOA的时钟
                                                            //使用各个外设前必须开启时钟,否则对外设的操作无效
    
    /*GPIO初始化*/
    GPIO_InitTypeDef GPIO_InitStructure;                    //定义结构体变量
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;        //GPIO模式,赋值为推挽输出模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;                //GPIO引脚,赋值为所有引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;        //GPIO速度,赋值为50MHz
    
    GPIO_Init(GPIOA, &GPIO_InitStructure);                    //将赋值后的构体变量传递给GPIO_Init函数
                                                            //函数内部会自动根据结构体的参数配置相应寄存器
                                                            //实现GPIOA的初始化
    
    /*主循环,循环体内的代码会一直循环执行*/
    while (1)
    {
        /*使用GPIO_Write,同时设置GPIOA所有引脚的高低电平,实现LED流水灯*/
        GPIO_Write(GPIOA, ~0x0001);    //0000 0000 0000 0001,PA0引脚为低电平,其他引脚均为高电平,注意数据有按位取反
        Delay_ms(100);                //延时100ms
        GPIO_Write(GPIOA, ~0x0002);    //0000 0000 0000 0010,PA1引脚为低电平,其他引脚均为高电平
        Delay_ms(100);                //延时100ms
        GPIO_Write(GPIOA, ~0x0004);    //0000 0000 0000 0100,PA2引脚为低电平,其他引脚均为高电平
        Delay_ms(100);                //延时100ms
        GPIO_Write(GPIOA, ~0x0008);    //0000 0000 0000 1000,PA3引脚为低电平,其他引脚均为高电平
        Delay_ms(100);                //延时100ms
        GPIO_Write(GPIOA, ~0x0010);    //0000 0000 0001 0000,PA4引脚为低电平,其他引脚均为高电平
        Delay_ms(100);                //延时100ms
        GPIO_Write(GPIOA, ~0x0020);    //0000 0000 0010 0000,PA5引脚为低电平,其他引脚均为高电平
        Delay_ms(100);                //延时100ms
        GPIO_Write(GPIOA, ~0x0040);    //0000 0000 0100 0000,PA6引脚为低电平,其他引脚均为高电平
        Delay_ms(100);                //延时100ms
        GPIO_Write(GPIOA, ~0x0080);    //0000 0000 1000 0000,PA7引脚为低电平,其他引脚均为高电平
        Delay_ms(100);                //延时100ms
    }
}

大多数 STM32 开发板的 LED 采用 低电平点亮(共阳极接法):

  • GPIO 输出低电平(0) → LED 两端有电压差 → LED 亮
  • GPIO 输出高电平(1) → LED 两端无电压差 → LED 灭


实验二:蜂鸣器实验

1 蜂鸣器简介

蜂鸣器是一种一体化结构的电子讯响器,采用直流电压供电,广泛应用于计算机、打印机、 复印机、报警器、电子玩具、汽车电子设备、电话机、定时器等电子产品中作发声器件。蜂鸣 器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。

这里的有源不是指电源的“源”,而是指有没有自带震荡电路,有源蜂鸣器自带了震荡电路, 一通电就会发声;无源蜂鸣器则没有自带震荡电路,必须外部提供2~5Khz 左右的方波驱动, 才能发声。

2 硬件设计

3 软件设计

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
    /*开启时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);    //开启GPIOB的时钟
                                                            //使用各个外设前必须开启时钟,否则对外设的操作无效
    
    /*GPIO初始化*/
    GPIO_InitTypeDef GPIO_InitStructure;                    //定义结构体变量
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;        //GPIO模式,赋值为推挽输出模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_12;                //GPIO引脚,赋值为第12号引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;        //GPIO速度,赋值为50MHz
    
    GPIO_Init(GPIOB, &GPIO_InitStructure);                    //将赋值后的构体变量传递给GPIO_Init函数
                                                            //函数内部会自动根据结构体的参数配置相应寄存器
                                                            //实现GPIOB的初始化
    
    /*主循环,循环体内的代码会一直循环执行*/
    while (1)
    {
        GPIO_SetBits(GPIOB, GPIO_Pin_0);
        Delay_ms(100);        //将PA0引脚设置为高电平
        GPIO_ResetBits(GPIOB, GPIO_Pin_12);        //将PB12引脚设置为低电平,蜂鸣器鸣叫
        Delay_ms(100);                            //延时100ms
        GPIO_SetBits(GPIOB, GPIO_Pin_12);        //将PB12引脚设置为高电平,蜂鸣器停止
        Delay_ms(100);                            //延时100ms
        GPIO_ResetBits(GPIOB, GPIO_Pin_12);        //将PB12引脚设置为低电平,蜂鸣器鸣叫
        Delay_ms(100);                            //延时100ms
        GPIO_SetBits(GPIOB, GPIO_Pin_12);        //将PB12引脚设置为高电平,蜂鸣器停止
        Delay_ms(700);
        GPIO_ResetBits(GPIOB, GPIO_Pin_0);    
        Delay_ms(100);        
    }
}

Logo

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

更多推荐