(1)单片机特点:体积小、功耗低、集成度高、使用方便、扩展灵活
(2)CISC和RISC的区别:
对比项 | 复杂指令集CISC | 精简指令集RISC |
---|---|---|
目的 | 为了便于编程和提高存储器访问效率 | 为了提高处理器的运行速度 |
特点 | - 指令多、模式多、格式可变 - 指令的时钟周期差距很大 - 无流水线或流水线程度较低 - 指令由微代码翻译执行 | - 指令少、模式少、格式固定 - 指令只需 1 个时钟周期 - 流水线结构 - 指令由硬件执行 |
优点 | - 指令丰富、功能强大 - 寻址方式灵活 | - 指令精简、易于设计、使用率均衡 - 程序执行效率高 |
缺点 | - 指令使用率不均衡 - 不利于采用先进结构提高性能 - 结构复杂,不利于超大规模集成电路实现 | - 指令少较少、功能不如CISC - 寻址方式不够灵活 |
举例 | 8051、X86 | ARM、MIPS、RISC-V |
(3)冯诺依曼结构与哈佛结构的区别
对比项 | 冯诺依曼结构 | 哈佛结构 |
---|---|---|
特点 | 程序指令和数据被存储在同一个存储器中 | 程序指令和数据被存储在两个独立的存储器中 |
优点 | 总线资源占用少 | 执行效率较高 |
缺点 | 执行效率较低 | 总线资源占用多 |
(4)cortex内核分类
对比项 | Cortex - A(应用) | Cortex - R(实时) | Cortex - M(微处理器) |
---|---|---|---|
特点 | 高时钟频率,长流水线,高性能 | 较高时钟频率,较长流水线,实时性强 | 低时钟频率,较短流水线,超低功耗 |
应用场景 | 移动计算、智能手机、平板电脑、数字电视 | 军工、汽车电子、无限基带、硬盘控制器 | 工控、传感器、消费电子、家用电器、医疗器械 |
(1)需要追踪程序执行时间时设置,根据系统时钟频率的实际值设置:
STM32F1 --- 72MHz
STM32F407 --- 168MHz
(2)仿真报错解决方法:
仿真结束前将所有断点清除
将工程路径改浅,并改成全英文路径
(3)工具栏常用窗口按钮结束
Call Stack窗口:查看函数调用关系和局部变量
Watch窗口:查看函数首地址和变量值;设置全局变量在被读或被写后自动停止运行
Memory窗口:内存查看窗口(M3 / M4 / M7 内核是小端模式)
Peripheral窗口:查看寄存器的值(确定配置寄存器是否有问题)
注意:
仿真时,使用 MDK 的 Level 0 等级优化
调试停止在断点处时,只是内核停止,外设会继续运行
断点的设置要有时间观念,考虑是否会打断正常通信
(1)stdint.h
从C99中引进的标准C库文件,推荐使用符合C99标准的 uint8_t、uint16_t、uint32_t等类型别名,需配置MDK支持C99(勾选 C/C++ -> C99 Mode)。
(2)位操作运算符
运算符 | 含义 |
---|---|
& | 按位与 |
| | 按位或 |
^ | 按位异或 |
~ | 按位取反 |
<< | 左移 |
>> | 右移 |
给寄存器某个位赋值:如 uint32_t temp = 0;
给位6赋值为0:
temp &= 0xFFFFFFBF ; temp |= 0x00000040
temp &= ~ (1 << 6) ; temp |= 1 << 6
(3)宏定义
提高效率、可读性、易改性(核心是替换)。
格式:#define 标识符 字符串
标识符:宏定义的名字
字符串:常数、表达式、格式串等
带参数的宏定义:利用 do{...} while(0) 来构造,这样不会受到大括号、分号、运算优先级等影响
如:
#define LED1(x) do{ x ? HAL_GPIO_WritePin ( LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_SET) : HAL_GPIO_WritePin ( LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_RESET}; } while(0)
(4)条件编译
指令 | 作用 |
---|---|
#if | 编译预处理条件指令,类似 if |
#ifdef | 判断某个宏是否已被定义( #ifdef + 宏名) |
#ifndef | 判断某个宏是否未被定义( #ifndef + 宏名) |
#elif | 若前面的条件不满足,则判定新的条件,类似 else if |
#else | 若前面的条件不满足,则执行后面的语句,类似 else |
#endif | #if / #ifdef / #ifndef的结束标志 |
头文件的条件编译:
#ifndef __LED_H
#define __LED_H
#include
code
#endif
代码的条件编译:
#if SYS_SUPPORT_OS
code
#endif
#ifdef 标识符
程序段1
#else
程序段2
#endif
注意:
当标识符已经被定义过(一般使用 #define 定义),对程序段 1 进行编译;否则对程序段 2 进行编译或结束。
(5)extern 声明
放在变量/函数前,表示该变量/函数已在其他文件定义,以便本文件引用。
extern 申明变量可以多次,但定义只有一次。
(6)typedef类型别名
为现有数据类型创新一个新的名字 / 类型别名,简化变量定义(定义结构体的类型别名和枚举类型)。
格式:typedef 现有类型 新名字
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
类型别名的应用:
Struct GPIO_TypeDef
{
__IO uint32_t CRL;
__IO uint32_t CRH;
......
};
Struct GPIO_TypeDef gpiox
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
......
}GPIO_TypeDef;
GPIO_TypeDef gpiox
(7)结构体
由若干基本数据类型集合组成的一种自定义数据类型,也叫聚合类型。
声明结构体类型:
struct 结构体名
{
成员列表;
}变量名列表(可选);
struct student
{
char *name;
int num;
int age;
int group;
float score;
}stu1,stu2;
typedef struct
{
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
}GPIO_InitTypeDef;
注意:
由结构体变量名引用其成员:结构体变量名 . 成员名
结构体指针成员变量引用方法:指针变量名 -> 成员名
(8)指针(内存地址)
指针变量:保存了指针的变量。
格式:类型名 * 指针变量名
char *p_str = "This is a test!";
*p_str:取 p_str 变量的值;
&p_str:取 p_str 变量的地址。
uint8_t buf[5] = {1,3,5,7,9};
uint8_t *p_buf = buf;
*p_buf = ? // 1
p_buf[0] = ? // 1
p_buf[1] = ? // 3
p_buf++;
*p_buf = ? // 3
p_buf[0] = ? // 3
指针使用的两大问题:
未分配(申请)内存(使用指针前必须初始化)
char *p_buf;
p_buf[0] = 100;
p_buf[1] = 120;
p_buf[2] = 150;
越界使用
uint8_t buf[5] = {1,3,5,7,9};
uint8_t *p_buf = buf;
p_buf[5] = 200;
p_buf[6] = 250;
(9)C 语言中的 u/U
0U:无符号整型 0
1U:无符号整型 1
如果不写 u/U后缀,系统默认为 int 型,即为有符号型整数。
(1)STM32 寻址范围
1)32 位单片机有 32 根地址线(每根地址线有导通或不导通两种状态)
2)单片机内存地址访问的存储单元是按字节编址的(不是 bit )
地址线根数 | 地址编号数(内存大小) |
---|---|
1 | 2 (0, 1) |
2 | 4 (00, 01, 10, 11) |
3 | 8 (000, 001, 010, 011, 100, 101, 110, 111) |
... | ... |
n | 2^n |
STM32寻址大小:232 = 4G(字节)
STM32寻址范围:0x0000 0000 ~ 0xFFFF FFFF
(2)存储器映射
1)存储器:存储数据的设备,存储器本身没有地址信息
2)存储器映射:对存储器分配地址的过程
3)存储器功能划分:(将 4GB 地址空间分为 8 个块,重点是前三个块)
存储块 | 功能 | 地址范围 |
---|---|---|
Block 0 | Code (FLASH) | 0x0000 0000 ~ 0x1FFF FFFF (512MB) |
Block 1 | SRAM | 0x2000 0000 ~ 0x3FFF FFFF (512MB) |
Block 2 | 片上外设(APB1、APB2、AHB) | 0x4000 0000 ~ 0x5FFF FFFF (512MB) |
(3)寄存器映射
1)寄存器:实现对单片机各个功能的控制(寄存器是单片机内部的控制结构)
2)分类:内核寄存器、外设寄存器
3)寄存器映射:给寄存器地址命名
如:0x4001080C(寄存器地址)➡️ GPIOA_ODR(寄存器名字)
4)寄存器描述:
寄存器名字
偏移量及复位值
寄存器位表
位功能描述
5)寄存器地址计算:
总线基地址 ( BUS_BASE_ADDR )
外设基于总线基地址的偏移量 ( PERIPH_OFFSET )
寄存器相对外设基地址的偏移量 ( REG_OFFSET )
寄存器地址 = BUS_BASE_ADDR + PERIPH_OFFSET + REG_OFFSET
外设基地址(PERIPH_BASE):APB1总线的基地址
例如:
(a) STM32F103 中 GPIOA_ODR 寄存器地址计算过程:
GPIOA 在 APB2 总线上,APB2 总线基地址:0x4001 0000
GPIOA 基于 APB2 总线基地址的偏移量:0x4001 0800 - 0x4001 0000 = 0x0800
ODR 寄存器基于 GPIOA 外设基地址的偏移量:0x0C
寄存器地址 = 0x4001 0000 + 0x0800 + 0x0C = 0x4001 080C
(b) STM32F4047 中 GPIOA_ODR 寄存器地址计算过程:
GPIOA 在 AHB1 总线上,AHB1 总线基地址:0x4002 0000
GPIOA 基于 AHB1 总线基地址的偏移量:0
ODR 寄存器基于 GPIOA 外设基地址的偏移量:0x14
寄存器地址 = 0x4002 0000 + 0 + 0x14 = 0x4001 0014
6)使用结构体,完成对寄存器的映射
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR
__IO uint32_t ODR
__IO uint32_t BSRR
__IO uint32_t BRR
__IO uint32_t LCKR
}GPIO_TypeDef;
#define GPIOA ((GPIOA_TypeDef *)GPIOA_BASE)
GPIOA_BASE: 0x4001 0800
&GPIOA->CRL: 0x4001 0800
&GPIOA->CRH: 0x4001 0804
&GPIOA->IDR: 0x4001 0808
&GPIOA->ODR: 0x4001 080C
&GPIOA->BSRR: 0x4001 0810
&GPIOA->BRR: 0x4001 0814
&GPIOA->LCKR: 0x4001 0818
实际应用:
GPIOA->ODR = 0xFFFF;
(1)MDK编译过程文件(11种)
文件类型 | 说明 |
---|---|
.o | 可重定向对象文件,每个源文件(.c / .s 等)编译都会生成一个 .o 文件 |
.axf | 可执行对象文件,由 .o 文件链接生成 .axf 文件,仿真时用到 |
.hex | 用于下载到 MCU 中,由 .axf 文件转换而来 |
.map | 连接器生成的列表文件,对分析程序存储占用情况非常有用 |
其他 | .crf、.d、.dep、.lnp、.lst、.htm、.build_log.htm一般用不到 |
注意:
可重定向:该文件包含数据/代码,但没有指定地址,可由后续链接时再指定;
不可重定向:该文件的数据/代码已经指定地址了,不能再改变。
(2)MAP文件
1)概念:MDK编译代码后,产生集程序、数据和IO空间的一种映射列表文件,包含交叉链接信息(各种 .c 文件、函数、符号等地址、大小、引用关系等信息)
2)作用:分析各个 .c 文件占用 FLASH(ROM)和 RAM 的大小,方便优化代码
3)组成:
组成 | 说明 |
---|---|
程序段交叉引用关系 | 描述各文件之间函数的调用关系 |
删除映像未使用的程序段 | 描述工程中未用到而被删除的冗余程序段(函数/数据) |
映像符号表 | 描述各符号(程序段/数据)在存储器中的地址、类型、大小等 |
映像内存分布图 | 描述各个程序段(函数)在存储器中的地址及占用大小 |
映像组件大小 | 给出整个映像代码 .o 占用空间汇总信息 |
概念 | 说明 | 占用空间 |
---|---|---|
RO (Read Only) | 只读数据 RO data + 代码 RO code | FLASH |
RW (Read Write) | 可读写数据 RW data(有初值,且不为0) | FLASH(存储初值)+ RAM(读写操作) |
ZI (Zero Initialized) | 初始化为 0 的数据 ZI data | RAM |
(1)软件功能:图形配置软件,可通过配置自动生成初始化代码
(2)安装步骤:
STM32CubeMX 软件获取
搭建 Java 运行环境
安装 STM32CubeMX 软件
下载并关联 STM32Cube 固件包 ( Help->Manage embedded software packages->install )
(3)新建 STM32CubeMX 工程
工程初步建立:新建工程,选取芯片型号
时钟模块配置:设置高速外部时钟 HSE(频率范围为 4MHz~16MHz )、低速外部时钟 LSE(接频率为 32.768KHz 的石英晶体)、MCO
时钟系统配置:锁相环 PLL、系统时钟 SYSCLK、高速总线 AHB、低速总线 APB1、APB2等
GPIO 引脚配置
Cortex 内核配置:SYS ( DEBUG) 配置、NVIC(优先级分组)
生成工程源码:设置工程、MDK等,最后生成代码工程
编写用户程序:在main.c文件预留的位置编写代码 ( USER CODE BEGIN 3~USER CODE END 3 )
(1)时钟:具有周期性的脉冲信号,最常用的是占空比为 50% 的方波
(2)认识时钟树:
F1系列:
时钟源名称 | 频率 | 材料 | 用途 |
---|---|---|---|
高速外部振荡器 HSE | 4~16MHz | 晶体/陶瓷 | SYSCLK / RTC |
低速外部振荡器 LSE | 32.768KHz | 晶体/陶瓷 | RTC |
高速内部振荡器 HSI | 8MHz | RC | SYSCLK |
低速内部振荡器 LSI | 40KHz | RC | RTC / IWDG |
F407系列:
时钟源名称 | 频率 | 材料 | 用途 |
---|---|---|---|
高速外部振荡器 HSE | 4~26MHz | 晶体/陶瓷 | SYSCLK / RTC |
低速外部振荡器 LSE | 32.768KHz | 晶体/陶瓷 | RTC |
高速内部振荡器 HSI | 16MHz | RC | SYSCLK |
低速内部振荡器 LSI | 32KHz | RC | RTC / IWDG |
备注:F7 和 H7 时钟树跳过
(3)系统时钟配置步骤
配置 HSE_VALUE:stm32xxxx_hal_conf.h,告诉 HAL 库外部晶振频率
调用SystemInit() 函数(可选):在启动文件中调用,在system_stm32xxxx.c定义
选择时钟源,配置PLL:通过 HAL_RCC_OscConfig() 函数设置
选择系统时钟源,配置总线分频器:通过 HAL_RCC_ClockConfig() 函数设置
配置扩展外设时钟(可选):通过 HAL_RCCEx_PeriphCLKConfig() 函数设置
(4)外设时钟使能和失能
使能:
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟
失能:
__HAL_RCC_GPIOA_CLK_DISABLE(); //禁止GPIOA时钟
(1)printf 函数输出流程
printf() 用户调用 ➡️ C 标准库( printf 部分,由编译器提供的 stdio.h 解析) ➡️ fputc() 最终实现输出
printf重定向:用户需要根据最终输出的硬件重新定义该函数
(2)printf 的使用
printf("字符串 \r\n");
printf("输出控制字符 \r\n",输出参数);
printf("输出控制字符1 输出控制字符 2 ... \r\n",输出参数 1,输出参数 2,... );
printf("非输出控制符 输出控制符 非输出控制符 \r\n",输出参数);
如何输出 %、\ 、""
printf("%%\r\n");
printf(" \\ \r\n");
printf(" \"\" \r\n");
(3)printf 的支持
避免使用半主机模式
微库法:魔术棒 -> Target 选项卡 -> 勾选 "Use MicroLIB"
代码法
实现 fputc 函数
#include
UATR_HandleTypeDef huart1;
int fputc(int ch,FILE *f)
{
HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,0xffff);
return ch;
}
文章参考:
第1讲 基础篇-单片机简介_哔哩哔哩_bilibili