前记:I2C之前板子是魔女F407,从I2C开始板子换成野火F103。F4和F1在外部中断EXTI的寄存器上面有些不一样
什么是寄存器
判断芯片引脚标号顺序
- 芯片上有小点的,把小点放在左上角,左边最上面的一个引脚是1,按逆时针递增。
- 没有小点的,正对芯片上的丝印,左边最上边的是第一个是1,逆时针递增
STM32命名规则
自己的板子是F103VET6,基础型100脚512k闪存QFP封装
Fundamental基础型 103基础型 V100脚 E 512kFLASH(RAM) T QFT方型扁平式封装 6温度范围
STM32芯片架构简图
Flash:存程序代码,通常说的内存不够了,就是指这个flash不够了。相当于ROM
SRAM:存变量。相当于内存条
关于flash、sram的更详细点的文章:STM32深入系列01——内存简述(Flash和SRAM)
存储器映射(Memory mapping)寄存器映射(Register mapping)
存储器映射 : stm32是在CM4内核32位基础上设计的,232bit = 4GB。CM4内核对4GB的空间进行了预定义,他有空间存储4GB的数据,但是这些存数据的地方并没有地址。为了能够往这些空间里读写东西,需要给这些空间分配地址以便于访问。给存储器分配地址的过程就叫存储器映射。
- 在data sheet里,4Memory mapping可以看到对应的存储器映射的表
可以看出,4GB的空间被分为了8block,每块512MB。但是ST公司在设计STM32芯片的时候,对CM4架构的4G空间的每个块只用到了很少一部分,其余部分都作为保留。外设的寄存器都放在block2中
寄存器映射 : 给存储器分配地址之后,给有特定功能的内存单元取一个别名的过程叫寄存器映射,这个别名就是常说的寄存器。
在参考手册里,2.3存储器映射可以看到外设挂载在哪些总线(AHB、APB叫总线)上,GPIOA在0x4002 0000 - 0x4002 03FF这块内存中设计。我们想要给GPIOA_ODR这块位置全赋值1时
0x4002 0000+0x14 = 0xFFFF; // 给这块地址写1
// 但是机器不知道 0x4002 0000+0x14 是地址,只会认为他是立即数,这种操作是非法的。
// 要想正常赋值,需要将他转换为地址,让编译器知道
(unsigned int *)0x4002 0000+0x14 = 0xFFFF;
// 这样也是不对的,因为向地址中赋值也是非法的
*(unsigned int *)0x4002 0000+0x14 = 0xFFFF; // 这样才能向地址中全赋值1
// 但是这样操作太麻烦,每次都要查询他的地址,给开发、阅读、维护带来很大的麻烦
#define GPIOA_ODR *(unsigned int *)0x4002 0000+0x14
// 这样的话,每次对GPIOA_ODR操作,就可以代替后面一串地址,极大地方便了编程
// define这个过程就叫寄存器映射
C语言对寄存器的封装
如果一个个去查地址和偏移地址,完成这些寄存器映射,太过于麻烦,代码量很大
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB1PERIPH_BASE PERIPH_BASE
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)
#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400)
#define GPIOF MODER (GPIO BASE+0X00)
#define GPIOF OTYPER (GPIOF BASE+OX04)
#define GPIOE OSPEEDR (GPIOE BASE+OX08)
#define GPIOF PUPDR (GPIOF BASE+OX0C)
#define GPIOE IDR (GPIOF BASE+OX10)
#define GPIOF ODR ...
#define GPIOE BSRR ...
#define GPIOE ICKR ...
#define GPIOF AFRI ...
#define GPIOF AFRH ...
*(unsigned int *)GPIOF_BSRR = (0x01<<6); // 将GPIOF_BSRR宏的数值,强行转换为地址,通过*访问地址,来对地址赋值
// 1 MB=1024 Kb
// 1 Kb=1024 Byte
// 1 Byte=8 Bit 通过观察发现,每个寄存器都占用了4个byte且是连续的。这和结构体成员内存分布有相似性。
/* C语言中,int类型的大小是由编译器和计算机体系结构决定,一般32位系统是4字节,64位系统是8字节 */
/* 在对于32位的ARM Cortex-M 架构,比如Cortex-M3 M4,int占4个字节,short int 占2字节 */
typedef unsigned int uint32_t; // int 4字节32位
typedef unsigned short int uint16_t; // short int 2字节16位
typedef struct
{
uint32_t MODER;
uint32_t OTYPER;
uint32_t OSPEEDR;
...
}GPIO_TypeDef
这样我们定义好了一个GPIO_TypeDef结构体后,并且这个结构体的首地址为 GPIOF BASE,那么结构体中第一个成员变量MODER的地址就是GPIOF BASE,第二个成员变量OTYPER的地址就是GPIOF BASE + 0x04,以此类推。这样只需要确定好结构体的首地址,就能很方便的访问这些寄存器了。
GPIO_TypeDef* GPIOF; // 定义一个GPOP_TypeDef的指针
GPIOF = GPIOF BASE; // 让这个指针指向GPIOF的首地址 0x4002 1400
GPIOF->MODER = 0xFFF; // 这样就很方便的对寄存器赋值最后,更进一步,直接使用宏定义好的GPIO_TypeDef类型的指针,指针指向每个GPIO的首地址,这样我们直接使用就好了。
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)固件库就是用这种方式进行封装的
位操作
/* C语言中,int类型的大小是由编译器和计算机体系结构决定,一般32位系统是4字节,64位系统是8字节 */
/* 在对于32位的ARM Cortex-M 架构,比如Cortex-M3 M4,
int 4字节,
short int 2字节
char 1字节
float 4字节
double 8字节
*/
// 二进制 a = 1001 1111 b
unsigned char a = 0x9f;
/* 1. 对某一位清零
对bit2清零
括号中的 1 左移两位(左移0位就是没移动,就是1;左移2位就是移到从0开始的第2位),(1<<2) 得二进制数:0000 0100 b
按位取反,~(1<<2) 得 1111 1011 b
和a进行&操作 1001 1111 b
a 的 bit2 位被清零,而其它位不变
*/
a &= ~(1<<2);
/* 2. 对连续几位清零
若把 a 中的二进制位分成 2 个一组。
即 bit0、bit1 为第 0 组,bit2、bit3 为第 1 组,bit4、bit5 为第 2 组,bit6、bit7 为第 3 组
要对第 1 组的 bit2、bit3 清零
上述 (~(3<<2*1)) 中的 (1) 即为组编号; 如清零第 3 组 bit6、bit7 此处应为 3
括号中的 (2) 为每组的位数,每组有 2 个二进制位;若分成 4 个一组,此处即为 4
括号中的 (3) 是组内所有位都为 1 时的值;若分成 4 个一组,此处即为二进制数“1111 b”
即 ~( 组最大值 << 组成员个数 * 第几组 )
*/
a &= ~(3<<2*1);
/* 3. 对某位置1 */
a |= (1<<2); // 第2位置1,其余不变
a |= (1<<2*2); // 此时对清零后的第 2 组 bit4、bit5 设置成二进制数“01 b
// (想置位的值 << 组成员个数 * 第几组 )
/* 4. 对某位取反 */
a ^=(1<<6); // 第6位取反,其余不变。异或操作,对应位相同取1,不同取0通用输入输出GPIO
(general purpose input output)
通用输入输出,是软件可控的引脚,stm32芯片的GPIO引脚与外部外设链接,实现通信、控制、数据采集等功能
GPIO和引脚的区别
不是所有的引脚都是GPIO,GPIO一定是引脚
引脚的功能查询
在data sheet中,3.Pinouts and pin description Table7查看引脚功能
GPIO功能框图
最右端正方形就是代表 STM32 芯片引出的 GPIO 引脚,其余部件都位于芯片内部
1.保护二极管、上下拉电阻
保护二极管:可以防止外部过高、过低电压的输入。只是一个比较弱的保护,如果直接驱动电机这样的大功率器件,还是会烧坏芯片。
上下拉电阻:可以确定IO口输入模式时的初始状态。上拉模式为高电平,下拉模式为低电平。如果都不设置,则称为 ”浮空模式“ 。此时引脚输入状态容易受到干扰,实际使用不建议浮空(按键功能可以使用),或者设置浮空输入,在外部加上下拉电阻,使引脚初始状态确定。通过上下拉寄存器GPIOx_CRL和CRH寄存器来配置这三种输入模式。
在配置时,CRL寄存器上并没有区分上下拉模式。参考表17发现,是ODR的值不同加以区分。所以只配置寄存器时,不仅要配置CRL寄存器,还要配置ODR寄存器。用固件库函数的话,只需要填好初始化结构体就可以了。
GPIOA->CRL|=0X00000008; //PA0 输入
GPIOA->ODR|=0<<0; //PA0 下拉输入
内部的上拉是弱上拉,即通过此上拉输出的电流是很弱的,如要求大电流还是需要外部上拉。
2.P-MOS管和N-MOS管
通过2的电路控制GPIO的推挽或者开漏两种输出模式。
推挽输出 : 可以输出0或1,驱动能力较强。
推挽输出一般用于输出电平为0和3.3V且需要高速切换开关状态的场合。除了必须开漏输出的时候,我们都选择推挽输出。
开漏输出 : 只可以输出低电平0和高阻态。如果想要输出高电平,需要外接上拉电阻,由外部电源输出高电平
如果引脚既不输出低电平,也不输出高电平,此时的状态为高阻态。如果在此时想要输出高电平,就需要外接上拉电阻。
当很多个开漏输出的引脚连接在一起时,只有他们都输出高阻态时,才可能输出高电平,由可能外接的上拉电阻提供。否则只要有一个引脚输出低电平,那么整条线都是低电平。这就是开漏输出的“线与”特性。
开漏输出一般应用在 I2C、SMBUS 通讯等需要“线与”功能的总线电路中。如果要输出高于3.3v的电压,则可以接一个5v电源和上拉电阻,设置为开漏输出,这样就能输出高于3.3v的5v电压了。
3.施密特触发器
整流,把输入的实际电压,转换为单片机内的数字电压0和1
GPIO寄存器
-
推挽输出(默认、常用)
- 输出1: 3.3V
- 输出0: 0V
-
开漏输出(PMOS始终截止):
- 输出1(NMOS截止):高阻态(无穷大电阻)(两个MOS管都导通,相当无穷大的电阻接地)(若接上拉电阻,则可输出高电平)(具有“线与”功能)
- 输出0(NMOS导通): 0V
-
浮空输入:引脚什么都不接,电平状态由外部电路决定。但是没有接入外部电路时,引脚状态不稳确定
-
输入上拉:没有信号输入时,默认就是VDD高电平。输入高电平读取还是高电平,输入低电平读取时低电平
- 没有信号输入时为高
-
输入下拉:没有信号输入时,默认就是VSS低电平。输入低电平读取还是低电平,输入高电平时读取高电平
- 没有信号输入时为低
-
复用模式:复用功能输出对应的单片机内部外设,如I2C,PWM、USART等,他们引脚电平变化频率太快。使用复用模式则对应的模块直接控制引脚。否则需要自己编程
- 想要驱动一些外设的时候(比如SPI),把一些引脚设置为了复用模式(SPI_NSS,SPI_SCK,SPI_MOSI,SPI_MISO),则这些引脚相应的GPIO设置也要设置为复用输出模式
GPIO初始化结构体
编程
// GPIO输出(点灯)
void LED_GPIO_Config(void)
{
// 打开led对应的GPIO的时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
// 定义GPIO初始化结构体,并设置其状态,再调用函数写入寄存器
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStruct.GPIO_Speed = GPIO_Low_Speed;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// 置位和清除
//GPIO_ResetBits(GPIOB, GPIO_Pin_2);
//GPIO_SetBits(GPIOB, GPIO_Pin_2);
}
// GPIO输入(按键)
void KEY_GPIO_Config()
{
// 打开led对应的GPIO的时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
// 定义GPIO初始化结构体,并设置其状态,再调用函数写入寄存器
GPIO_InitTypeDef GPIO_InitStruct;
//GPIO_InitStruct.GPIO_Speed = GPIO_Low_Speed; // 输入不需要速度
//GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; // 输入不需要配输出模式
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_DOWN; // 输入需要配一下上下拉确定初始状态
GPIO_Init(GPIOA, &GPIO_InitStruct);
}
STM32CUBEMX配置
Output Level:仅在输出模式可选。代表GPIO默认输出的电平
GPIO Pull up/Pull down:输出模式时没影响。输入模式时,把未知电平限制为高/低
RCC(复位和时钟 控制)
- RCC管理的都是内核之外的外设
- RCC管理的外设时钟默认都是关闭的
RCC在时钟方面的主要作用: 设置分频因子、控制总线和外设的时钟开启
对于F407来说,HCLK = SYSCLK=PLLCLK = 168M,PCLK1=HCLK/2 = 84M,PCLK1=HCLK/4 = 42M
时钟树
OSC:振荡器
HSE OSC (High Speed External):外部高速时钟
LSE OSC (Low Speed External):外部低速时钟
HSI OSC (High Speed Internal):内部高速时钟
LSI OSC (Low Speed Internal):内部低速时钟
- HSE外部高速时钟信号
- 锁相环PLL
- 系统时钟SYSCLK
- AHB总线时钟HCLK
HCKL = SYSCLK = 168MHz - APB2总线时钟PCLK2
PCLK2 = HCLK / 2 = 84MHz - APB1总线时钟PCLK1
PCLK1 = HCLK / 4 = 42MHz - 一般情况下,都按照最高频率工作。不同人做的板子,HSE晶振频率不同,需要改一下官方标准库中system_stm32f4xx.c里面PLL_M的分频系数
一般使用外部高速时钟HSE来给系统提供时钟。STM32CUBEMX默认是使用内部高速时钟HSI来给系统提供时钟的。要使用HSE,要配置RCC的HSE选择外部晶振。
SetSysClock()函数
就是在这个函数中对系统时钟进行一系列的设置。如果要修改系统时钟,需要自己改写
中断概览(NVIC,内核外设)
-
NVIC嵌套向量中断控制器,属于内核外设,管理所有内核与片上外设的中断相关功能。
- 内核的外设都不需要开启时钟
- 重要的库 文件 core_cm4.h,misc.c(主要看这个,里面有固件库函数)
- 官方参考资料:内核参考手册
-
只要有中断,就要配置NVIC相关的寄存器,对应的库函数在core和misc里,参考资料是STM32F10xxx Cortex-M3编程手册-英文版
-
优先级分组
- 抢占优先级和响应优先级(主优先级和子优先级),值越小,则优先程度上越高
- 先选优先级分组,常用第2组
NVIC寄存器
中断优先级寄存器 NVIC→IPRx
优先级分组 SCB→AIRCR:PRIGROUP[10:8]
- 主优先级高,则可以打断主优先级低的
- 主优先级相同,则子优先级不可相互打断。若同时发生,子优先级高的优先
- 主和子优先级均相同,则不可互相打断,谁先发生就先执行(虽然说应该看中断向量表的编号)
NVIC初始化结构体
⭐中断编程顺序
开启时钟→ RCC
配置外设相关寄存器
- 如果有中断相关,不要忘记打开
配置NVIC
先定下NVIC优先级分组(只需要配置1次)NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
- STM32上电之后,优先级分组是不确定的,所以必须明确优先级的分组(SCB寄存器)
- 内核中只有3个中断不可设置,不受分组的影响。其余的内核中断优先级都可以配置,受分组影响
配置NVIC初始化结构体,选择中断源、主次优先级、使能(有几个中断源,就要配置几次)
如果是EXTI,要先打开SYSCFG时钟,选择EXTI线,再配置EXTI初始化结构体(有几根中短线配置几次)
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
- SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource4);
在it.c中编写中断服务函数
- 所有的中断服务函数,在启动文件中都有weak定义,其执行的命令就是让程序停止。开启了中断但是没有自己写相应的中断服务函数,则会默认执行这些weak定义的函数,系统会卡死在那里。
- 名字必须和中断向量表里的一样,在启动文件中
- 进入中断服务函数先判断标志位,出中断再清除标志位。
外部中断/事件控制器 EXTI
外部中断,指的是相对于EXTI来说的外部,比如RTC、USB、电源、GPIO引脚或者事件等,外部这些东西可以通过EXTI外设,来产生中断。最常用的就是GPIO引脚电平变化产生中断
外部中断/事件控制器包含多达 23 个用于产生事件/中断请求的边沿检测器。每根输入线都可单独进行配置,以选择类型(中断或事件)和相应的触发事件(上升沿触发、下降沿触发或边沿触发)。每根输入线还可单独屏蔽。挂起寄存器用于保持中断请求的状态线。
EXTI功能框图
(彩色这图是F1的,F4不是AFIO,是SYSCFG)
复用IO口AFIO(Alternative Function IO) 主要用于引脚复用功能的选择和定义,主要完成2个任务:复用功能引脚重映射、中断引脚的选择
在使用GPIO外部中断时,必须先使能系统配置控制器SYSCFG的时钟。根据上面F4的框图可知,输入线其实有23根(0-15共16个提供给IO,还有7根其他的),但是产生中断的引脚有很多(16xGPIO个数),如何选择哪一个引脚,作为中断源输入?
需要配置SYSCFG寄存器来选择检测哪个GPIO的Pin产生的中断,且每个Pin只能选择某一个GPIO的Pin。通过SYSCFG_EXTILineConfig函数来确定。如果指定了同一个GPIO,则后面的函数的引脚,会覆盖前面的引脚。
这解释了为什么相同的Pin不能同时设置中断
看这些寄存器的复位值可以看出,EXTI0、EXTI1、EXTI2、、、、他们的复位值都是0x0000,也就是说,每一根输入线,默认选择时是PA0、PA1、PA2、、、触发中断。所以即使没有配置SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSourcex);,都默认是GPIOA的引脚触发中断。
在STM32CUBEMX中,先设置了PA0的外部中断,再设置PB0的外部中断,那么CUBEMX会自动将PA0的外部中断取消,恢复到Reset状态,而保留PB0的中断设置
EXTI寄存器
EXTI初始化结构体
SYSCFG相关函数
中断编程
标准库:进入中断函数后,先判断中断标志位,再进行相关操作,最后清除中断标志位。
void EXTI4_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line4) != RESET)
{
LED1_TOGGLE;
}
EXTI_ClearITPendingBit(EXTI_Line4);
}系统定时器STK
systick
- 24位,只能递减
- 内嵌在NVIC中 (NVIC在内核中,这意味着不需要单独打开systick时钟)
- 所有Cortex-M的单片机都有这个定时器(意味着代码移植方便)
- 裸机情况下,最主要的功能就是计时。在操作系统中,用于产生任务调度的时钟
SysTick功能框图
在STK_CLK的驱动下,从STK_LOAD开始递减(最高从224开始),减到0
SysTick寄存器
校准数值寄存器一般用不到。时钟源一般选择AHB时钟
SysTick唯一的固件库函数
- 传入参数ticks是定时器开始递减时的值
- 首先判断了传入LOAD寄存器的数值,最高不可超过
2^24, - 配置中断优先级为最低
- 当前值寄存器设置为0
- 选择AHB时钟、使能中断、使能计时
编程
简单SysTick定时
首先调用唯一的固件库函数,传入参数,确定多久产生一次中断
- 一般不选择太快,否则系统一直在中断中。一般选择1ms中断一次,传入SystemCoreClock / 1000
然后再for循环中读取SysTick→CTRL寄存器COUNTFLAG的值
STM32CUBEMX的问题
在配置时钟树的时候,可以选STK的时钟频率是HCLK的1分频或者8分频等等,但是修改之后,代码无任何区别,因为HAL库最终还是通过固件库函数SysTick_Config(uint32_t ticks)来配置的,默认都是1分频。
如果想要修改,则需要在HAL_STSTICK_Config()结束后,自己使用SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8)函数修改。
通信
串行和并行
- 串行通讯是指设备之间通过少量数据信号线 (一般是 8 根以下),地线以及控制信号线,按数据位形式一位一位地传输数据的通讯方式
- 并行通讯一般是指使用 8、16、32 及 64 根或更多的数据线进行传输的通讯方式
不过由于并行传输对同步要求较高,且随着通讯速率的提高,信号干扰的问题会显著影响通讯性能,现在随着技术的发展,越来越多的应用场合采用高速率的串行差分传输
单工、双工
同步、异步
在同步通讯中,数据信号所传输的内容绝大部分就是有效数据,异步通讯中会包含有帧的各种标识符,所以同步通讯的效率更高,但是同步通讯双方的时钟允许误差较小,而异步通讯双方的时钟允许误差较大。
通信速率
-
比特率(Bitrate):每秒传输的二进制位数,单位bit/s、
-
波特率(Baudrate):每秒传输多少码元
- 码元:
USART
串口通讯 (Serial Communication) 是一种设备间非常常用的串行通讯方式。
USART是一个全双工通用同步/异步串行收发模块,该接口是一个高度灵活的串行通信设备。
串口通信协议
对于通讯协议,我们也以分层的方式来理解,最基本的是把它分为物理层和协议层。
物理层规定通讯系统中具有机械、电子功能部分的特性,确保原始数据在物理媒体的传输。
协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准。
简单来说物理层规定我们用嘴巴还是用肢体来交流,协议层则规定我们用中文还是英文来交流。
物理层
串口通讯的物理层有很多标准及变种,RS232、USB转串口、原生串口到串口。
RS232标准串口通讯结构图
RS232标准主要用于工业设备直接通信
电平转换芯片一般有MAX3232、SP3232
电平标准
电子电路中常用TTL电平,超过40cm抗干扰能力就很差了。RS232电平抗干扰能力较强。一般用电平转换芯片MAX3232、SP3232对 TTL与RS232电平进行信号转换
RS-232信号线
RS-232标准的COM口(也称为DB9接口)。虽然接口很多,但是大多是只用RXD、TXD和GND三条线,其他的都不用
USB转串口
- 主要用于设备和电脑通信
- 电平转换芯片一般有CH340、PL2303、CP2102、FT232
- 使用时,电脑需要安装对应芯片的驱动
原生的串口到串口
- GPS模块、GSM模块、串口转wifi模块
协议层
串口通讯的数据包由发送设备通过自身的 TXD 接口传输到接收设备的 RXD 接口。在串口通讯的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据
-
波特率:常用9600、115200
-
起始位:由一个逻辑0表示
-
有效数据:有效长度可以是5、6、7、8位,但是一般都约定8位
-
校验位:可选。奇校验、偶校验、0校验、1校验、无校验
-
停止位:由0.5、1、1.5或2个逻辑1表示,双方约定一致即可
- 默认使用 1 个停止位。2 个停止位适用于正常 USART 模式、单线模式和调制解调器模式。0.5 个和 1.5 个停止位用于智能卡模式。
USART功能框图
Universal Synchronous Asynchronous Receiver and Transmitter(USART),通用同步异步收发器。USART 支持使用 DMA,可实现高速数据通信。
STM32 的 USART 输出的是 TTL 电平信号,若需要 RS-232 标准的信号可使用 MAX3232 芯片进行转换
- 上面6个引脚是GPIO的复用功能,需要在
data sheet中3 Pinouts and pin description中的pin and ball definitions查找(pin and ball 就是引脚的意思,有的引脚是针状的,有的引脚是球状的)
USART寄存器
- USART_DR寄存器实际包含2个寄存器,TDR和RDR。
- 在编程中,如果写
USART_DR = data,那么这个data就放到TDR中(data一般是8位或者9位) - 如果是
data = USART_DR,那么就是从RDR读取数据
- 在编程中,如果写
- 当数据写入
TDR后,数据会从TDR一位一位地放入到发送移位寄存器中,然后再通过TX引脚发送出去-
- 当
TDR的数据全部传送到发送移位寄存器后,状态寄存器USART_SR中的TXE(发送数据寄存器空)位会置1。向USART_DR寄存器写入数据会将该位清零
- 当
- 当发送移位寄存器的数据通过
TX发送出去后,状态寄存器USART_SR中的TC(发送完成)位会置1。 - 如果打开
USART_CR1寄存器对应中断位,则会TXE和TC位置1时会产生中断
-
- 数据从RX来后先进入接收移位寄存器,接收移位寄存器满后,把数据传入
RDR- 当
RDR 移位寄存器的内容已传输到 USART_DR 寄存器时,USART_SR的RXNE位会置1。 - 如果打开
USART_CR1寄存器对应中断位,则RXNE位置1时会产生中断
- 当
- 在使用串口时,不仅要打开串口外设的时钟,还要使能串口,使能发送或接收
波特率计算
- 实际配置的时候,直接向固件库函数写要设置的波特率就行了
USART初始化结构体
还有一个用于同步通信的结构体,一般都不用同步通信
编程
-
常用的固件库函数
-
void GPIO_PinAFConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_PinSource, uint8_t GPIO_AF);指定引脚的功能。用这个函数来选择哪个引脚为TX哪个为RX -
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);串口使能 -
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);中断配置 -
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);获取中断状态位 -
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);发送数据 -
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);接收数据
-
-
1.初始化串口用到的GPIO
-
2.串口初始化
-
3.中断配置
-
4.使能串口
-
5.编写发送接收函数
-
6.编写中断服务函数
- 串口发送来的数据是字符,在处理的时候要当做字符来处理
- 串口中断服务函数只有一个。需要在中断服务函数中读标志位来确定触发的哪个中断。
- 有些中断标志位不需要手动清零
遇到的问题
- 发送一个字节的时候,
while中的USART_FLAG_TXE换成TC效果都一样。
- 发送字符串的时候,
while中的USART_FLAG_TXE换成TC,则只发送一次时,会吞掉第一个字节
通过查参考手册发现,TXE和TC的复位值,都是1。如果判断条件是TC的话,由此分析发送字节,其实while根本没有起到作用。但是能发送成功,是因为后面没有任何操作了,数据自然可以从移位寄存器中通过TX发送出去,不会被其他的数据覆盖。
如果判断条件是TC的话,TC的复位值是1,连续发送字符的时候,发送第一个字节时,while判断TCRESET就没有起作用,直接跳出了循环,然后立即被下一个字节的数据覆盖掉了,所以第一个字节的数据,就没有发送出去。那为什么第二个数据就可以了呢?看TC清零的条件(有两种方式):第一种为向该位写0,第二种是先读SR寄存器,再写DR寄存器。在while判断TCRESET时,就是读SR,下一次发送数据的时候,就是写DR,这样一来,TC就被清零了。所以第二次发送字节的时候,while中的判断就是有用的了。所以除了第一次会被吞,之后的数据都是正常的。
如果就想用TC的话,可以这么改,在发送字节之前,读一下SR
编码格式
串口发送的数据到串口助手,解码一般按照ASCII码、HEX解码。
在编程时想要发送数字,比如1、2、3…,如果选择ASCII解码,那么就是找十进制1、2、3…对应的ASCII符号,如果选择HEX解码,那么就显示16进制的数字,而不会直接显示想要看到的十进制数字
-
sendString这个函数中,输入形参是uint8_t类型的地址,想发送的字符串,是uint8_t myData[];对于这个数组的初始化方式有以下几种情况
- uint8_t myData1[] = “1234”; 这个数组会被初始化为 这个字符串中每个字符的ASCII码值,也就是1、2、3、4对应的49、50、51、52。这样的初始化方式等同于 uint8_t myData2[] = {‘1’, ‘2’, ‘3’, ‘4’}; 这两个数组储存的都是字符。如果你尝试将这个数组的元素当作整数来处理,你会得到它们的ASCII码值,而不是它们所代表的数字
- uint8_t myData3[] = {1, 2, 3, 4}; 这个数组中的每个元素都是无符号整数,而不是字符。因此,它们不包含ASCII码值,而是直接包含了你指定的整数值。
- 在使用串口的时候,我们通常都是发送字符串,也就是存在内存里,这些字符对应的ASCII码
-
但是许多传感器检测到的数据,都是uint8_t形式的无符号整数,如何将这些整数通过串口发送到上位机让我们读出来呢?
- 只需要将uint8_t myData3[] = {1, 2, 3, 4};这个数组中的每个无符号整数元素都和 ‘0’ 这个字符相加,就能把对应的
-
uint8_t 为 unsigned char。 uint_fast8_t 是 unsigned int。
直接存储器访问DMA
(Data Memory Acess)
直接存储器访问 (DMA) 用于在外设与存储器之间以及存储器与存储器之间提供高速数据传输。可以在无需任何 CPU 操作的情况下通过 DMA 快速移动数据。这样节省的 CPU 资源可供其它操作使用。
DMA 控制器基于复杂的总线矩阵架构,将功能强大的双 AHB 主总线架构与独立的 FIFO 结合在一起,优化了系统带宽。
-
DMA独立在内核之外,不是内核的外设。总共有2个。CPU给DMA发一个指令,DMA就很快的去传输数据,不需CPU干预,CPU可以做别的事
- 外设P一般指:外设的数据寄存器,比如ADC、SPI、I2C、DCMI等外设的数据寄存器
- 存储器M一般指:片内SRAM、片内FLASH、外部存储器
- DMA1:P-M,M-P(后面的M只能是SRAM)
- DMA2:P-M,M-P,M-M(后面的M只能是SRAM。其实也就相当于P-M了,只不过P是一块Memory )
DMA功能框图
外设选择通道
- 流(stream):是数据传输的链路。每个DMA有8条独立的数据流,每次(每趟)传输的最大数据量为65535,如果数据单位是是单字(4字节)的话,那么一次最大可以传输256kb
- 通道(channel):每个数据流有8个通道选择,每个通道对应不同的DMA请求。
- 每个外设对应的不同数据流的不同通道。
- 当使用DMA2进行M-M传输时,不需要选择,所有流的所有通道都可以使用
- 配置
DMA_SxCR寄存器来选择流和通道
仲裁器
有一个外设要用流1,有一个要用0,这两个同时发起请求。如何决定先处理哪个?
- 软件阶段:判断DMA_SxCR的PL优先级等级位,优先级高的先处理
- 硬件阶段:如果优先级一样,数据流编号小的优先级高
- 同一个DMA可以同时使用多个数据流,当数据流同时到来是,会由仲裁器决定先后顺序。
- 但同一个数据流只能同时使用一个通道
FIFO
-
先进先出存储器缓冲区,是外设与存储器之间的中转站。
-
每个数据流有单独的四级 32 位先进先出存储器缓冲区 (FIFO)
-
DMA_SxFCR寄存器控制FIFO相关的状态
-
DMA传输时使用直接模式或FIFO模式(在M-M传输时,会自动启用FIFO模式,无法禁止)
-
直接模式:数据进入FIFO后不停留,直接传入目标
-
FIFO模式:数据进入FIFO后先放着不动,等达到FIFO阈值后,再根据 突发传输配置 进行数据传输
- 数据打包发送的好处:直接发送,每次来1个单位的数据,指针每次都加一;打包发送,每来一包数据,指针加很多。打包发送时,每次写数据的时候,寻址次数相对于直接发送少很多,效率更高。
- 且数据打包发送中间不能被打断。即使数据流被关闭,本次数据仍要传送完才会停止。数据传输准确性更好
-
-
FIFO阈值设置(FIFO总容量的多少)
-
-
DMA_SxCR寄存器控制 存储器/外设 突发传输配置
-
FIFO缓冲区4级32位,共16字节(4字,8半字)
- M-M模式下,FIFO强制启用
DMA初始化结构体
常用固件库函数
-
void DMA_DeInit(DMA_Stream_TypeDef* DMAy_Streamx); 复位DMA的寄存器- 有时用完DMA后再开一个DMA,FIFO中可能有上次没有发完的数据,再发送可能导致数据有误
-
void DMA_Init(DMA_Stream_TypeDef* DMAy_Streamx, DMA_InitTypeDef* DMA_InitStruct); 初始化 -
void DMA_Cmd(DMA_Stream_TypeDef* DMAy_Streamx, FunctionalState NewState); 使能DMA
编程
M-M
内部FLASH数据传输到内部SRAM。内部FLASH存程序、常量的,不可修改,只读。内部SRAM存变量的,可以读写。
- const定义内部FLASH源数据,再定义一块SRAM目标区域
- 配置DMA
-
使能时钟
-
DeInit一下,确保FIFO中没有遗留的数据
- while (DMA_GetCmdStatus(DMA2_Stream0) != DISABLE);等待传输完成跳出while
-
初始化结构体
-
清除DMA数据流传输完成位和其他相关位
-
-
使能传输
-
- 等待传输完成
- DMA_LISR和DMA_HISR寄存器的TCIFx位(数据流 x 传输完成中断标志),1表示数据流x上有传输完成事件
- 传输完成后比较数据是否相同
M-P
- M-M,使能数据流后就开始搬运数据了
- M-P,则需要P发起请求
存储器
存储器按其存储介质特性主要分为“易失性存储器”和“非易失性存储器”两大类。指的是掉电之后,他存储的数据是否会丢失。 掉电易失的,在计算机里典型代表就是内存条 掉电不易失的,典型代表就是机械硬盘硬盘、固态硬盘
ROM在单片机里就是存放用户程序代码的,经典的ROM就是片内的FLASH。工作时CPU会从FLASH读取程序,在FLASH中运行 RAM在单片机里广泛用于存放各种临时数据,经典的RAM就是片内SRAM。当然也有RAM存储代码,在RAM中运行的,这样的运行速度更快
RAM(Random Access Memory)(掉电易失)
随机存储器,指的是读取和写入时需要的时间,与数据的位置无关。读取其内部任意位置的数据,需要的时间都是一样的。 实际上现在 RAM 已经专门用于指代作为计算机内存的易失性半导体存储器
DRAM(Dynamic RAM)
动态随机存储器 DRAM 的存储单元以电容的电荷来表示数据,有电荷代表 1,无电荷代表 0。但时间一长,代表 1 的电容会放电,代表 0 的电容会吸收电荷,因此他需要定期刷新操作,这就是Dynamic的含义 DRAM通讯方式,常用的是同步S DRAM。为了提升SDRAM的性能,又有了DDR SDRAM,又有了DDRII,DDRIII等
⭐️SRAM(Static RAM)
这种电路结构不需要定时刷新充电,就能保持状态。 SRAM通讯方式,异步SRAM用的多
所以在实际应用场合中,SRAM 一般只用于 CPU 内部的高速缓存 (Cache),而外部扩展的内存一般使用 DRAM
非易失存储器:ROM(Read Only Memory)FLASH
ROM
MASK ROM
OTPROM
EPROM(Erasable Programmable ROM)
EEPROM(Electrically Erasable Programmable ROM)
FLASH
FLASH 存储器又称为闪存,它也是可重复擦写的储器,部分书籍会把 FLASH 存储器称为 FLASHROM,但它的容量一般比 EEPROM 大得多,且在擦除时,一般以多个字节为单位。如有的 FLASH存储器以 4096 个字节为扇区,最小的擦除单位为一个扇区。根据存储单元电路的不同,FLASH存储器又分为 NOR FLASH 和NAND FLASH,
I2C协议
(Inter-Integrated Circuit)
从I2C开始,板子用野火F103了
I2C 通讯协议 (Inter - Integrated Circuit) 是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路 (IC) 间的通讯。
物理层
I2C通讯设备之间的常用连接方式
- 总线:多个设备共用的信号线
- 双向串行数据线SDA,串行时间线SCL。时钟线用于数据收发同步
- 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问
- 总线通过上拉电阻接到电源。当某个 I2C 设备空闲时,会输出高阻态。而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平
- 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
- 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式
- 一般用快速模式,标准模式有点慢。
- 对应的是SCL的频率,100kb/s则SCL频率为100KHz
- 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。
- 地址一般是27 也就是最高128个设备。但电容限制更为优先。
协议层
I2C 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节
I2C基本读写过程
【阴影部分:数据由主机到从机】【白色部分:数据由从机到主机】【S:传输开始信号】【A:应答/非应答】【P:传输停止信号】
复合读写过程
- 比如stm32想要读取EEPROM里某一个地址的数据。则先向EEPROM写,写入要读取的地址;再读,读EEPROM传回的数据
起始、停止信号
- SCL一直为高电平时
- 起始:SDA高变低
- 停止:SDA低变高
- 这两个信号一般由主机产生
- 没见过从机产生
数据有效性
- SCL为高时,数据有效。SCL为低时,数据无效,此时SDA做数据变换
- 每次数据传输都以字节为单位
地址及数据方向
- I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS) 来查找从机。I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。
- 紧跟设备地址的一个数据位用来表示数据传输方向。
- 1表示主机由从机读数据
- 0表示主机向从机写数据
- 要区分设备写地址 和 设备读地址
- 设备写地址 (7addr<<1) | 1
- 设备读地址 (7addr<<1) | 0
响应
作为数据接收端时,当设备 (无论主从机) 接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答 (ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答 (NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制SDA,
- 非应答信号 (NACK):SDA高电平1
- 应答信号 (ACK):SDA低电平0
STM32的I2C特性及架构
- 1通讯引脚、2时钟控制逻辑、3数据控制逻辑、4整体控制逻辑
- SMBus也是一种协议,好像I2C兼容了SMBus
通讯引脚
- 查询DataSheet中引脚的复用功能
时钟控制
- 主要配置I2C_CCR寄存器,计算CCR的值,来控制通讯速率
数据控制逻辑
-
SDA连接在数据移位寄存器上。数据移位寄存器的数据来源有:数据寄存器DR、地址寄存器OAR、PEC寄存器、SDA数据线。
- 发送数据时:数据移位寄存器把DR的数据一位一位的发出去
- 接受数据时:数据移位寄存器把SDA信号线采集的数据一位一位存到DR
- STM32工作在从机模式的时候,接收到设备地址信号时,数据移位寄存器把收到的地址与自身I2C地址寄存器的值作比较,以便做出响应
- STM32 的自身 I2C 地址可通过修改“自身地址寄存器”修改,支持同时使用两个 I2C 设备地址,两个地址分别存储在 OAR1 和 OAR2 中。
-
PEC寄存器是负责数据校验的,好像都不用。
整体控制逻辑
- 控制寄存器
- 状态寄存器
STM32的I2C通讯流程
默认情况下,I2C接口总是工作在从模式。从从模式切换到主模式,需要产生一个起始条件。
-
作为主机,发送数据
-
- EV后面跟的位都是状态寄存器中的位,判断这些位是为了保证数据都正确的发送处理了,不会因为STM32工作过快导致有些步骤没有完成就继续下一步导致通信出错
- 读完这些寄存器之后,需要清除这些位,避免下一次操作时产生干扰。每个寄存器的清除方式不一样,用库函数方便很多。
-
-
作为主机,接收数据
-
-
I2C寄存器
I2C初始化结构体
- 输入Speed,Init函数就自动选择标准或者快速模式了。
- 自身地址OwnAddress1,还有一个地址,需要调用库函数去设置
- 地址长度要和总线上的从机都统一,否则会出错
EEPROM(AT24C02(2K)为例)
- 2k的EEPROM,分为32page,每页8byte。
- A0-A2:地址输入
- WP:写保护(接VCC则不能写入,一般接地)
- SCL、SDA:I2C通信用
写操作
-
Byte Write 字节写入:
-
- 在主机STOP后,EEPROM进入到自身的写入周期tWR,在这个周期中,EEPROM不会响应任何输入,直至写入完成。
- ⭐️如何确认数据是否写入完成?主机发送START+DEVICE ADDRESS+R/W,只有EEPROM上一次写完了,才会回复ACK。此时主机发送STOP结束本次通信,再重新开始做想做的工作
-
-
Page Write 页写入:(突发写入)
-
- 2K的一次只能写入8个字节。EEPROM在接受到每个字节后回复ACK,接收完8字节后,主机必须发送STOP停止写入。
- 接收到每个数据字后,数据字地址较低的三个(1K/2K)或四个(4K、8K、16K)位在内部递增。较高的数据字地址位不递增,保留内存页行位置。当内部生成的字地址到达页边界时,下一个字节被放置在同一页的开头,覆盖之前的数据继续写入。如果超过8个(1K/2K)或16个(4K, 8K, 16K)数据字被传输到EEPROM,数据字地址将“翻转”,回到本页的开头,覆盖之前的数据继续写入(最好避免该情况发生)。
-
-
ACKNOWLEDGE POLLING
- 如何确认数据是否写入?主机发送START+DEVICE ADDRESS+R/W,只有EEPROM上一次写完了,才会回复ACK
读操作
- CURRENT ADDRESS READ 当前地址读取: 一般不用,因为很难确定当前的地址
-
RANDOM READ 随机读取: 和前面的I2C复合读写过程类似
-
- 先进行写操作,发送一个地址。接收到ACK后不STOP,而是继续发送一个START重新进行READ,这时EEPROM就会把该地址的数据发送来,此时主机回复NO AKC 并发送STOP结束信号。
-
-
SEQUENTIAL READ 顺序读取:
-
- 和上面类似,只是主机接收到第一个数据后,回复ACK继续接收下一个地址的数据,直至不想读取了,回复NO AKC 并 STOP
- 读取的字节数没有要求,但是当达到内存地址限制时,数据字地址将“roll over”继续读取(回到地址0开始读)(尽量不要超过)
-
存小数、字符等
和存整数一样的,不会有特殊的对待。只是读取时解码方式不同。
实际上,EEPROM只存放0、1这样的数据,在读出时,不同的解码方式读出的数据不同
printf("小数 rx = %LF \r\n",double_buffer[k]);
printf("整数 rx = %d \r\n",int_bufffer[k]);
printf("整数 rx = %c \r\n",char_bufffer[k]);SPI协议
(Serial Peripheral Interface) SPI 协议是由摩托罗拉公司提出的通讯协议,即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间,要求通讯速率较高的场合。
物理层
- SS:从设备选择信号线,常称片选信号线,NSS、CS
- 主机要选择从设备时,把对应从机的SS线设置为低电平0。SPI通信以SS拉低为开始,以SS拉高为结束
- SCK(Serial Clock):时钟信号线,用于时钟同步
- MOSI(Master Output, Slaver Input):主设备输出、从设备输入
- 这条线上 主机发送数据,从机 接受数据
- MISO(Master Input, Slaver Output):主设备输入、从设备输出
- 这条线上 从机发送数据,主机 接受数据
协议层
-
NSS、SCK、MOSI都由主机控制产生,MISO由从机产生。MOSI和MISO只有在NSS为低才有效,SCK每个周期MOSI和MISO传输1bit数据。
-
通讯的起始和停止信号:NSS由高1变低0代表从机被选中,通讯开始。NSS由低0变高1,代表通讯结束
-
数据有效性:MOSI 及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。
- 传输数据时LSB还是MSB先行没有硬性规定,只要通讯设备之间保持一致就可以。一般采用MSB先行。
- SCK上升沿时,MOSI和MISO在变化;SCK下降沿时对这两条线进行采样,记录数据(不是硬性规定的,可以修改何时何时触发和采样)
-
时钟极性CPOL:指的是SPI设备处于空闲状态(NSS为高)时,SCK信号线的电平状态
- CPOL=0,则SCK在空闲状态为低电平;CPOL=1,则SCK在空闲状态为高电平
-
时钟相位CPHA:指的是数据采样的时刻
- CPHA=0,则在SCK“奇数边沿”进行采样;CPHA=1,则在SCK的“偶数边沿”采样
-
时钟极性和时钟相位相互配合,可以设置在何时采样
| SPI模式 | CPOL | CPHA | 空闲时SCK时钟 | 采样时刻(奇偶) | 采样时刻(沿) |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 低电平0 | 奇 | 上升 |
| 1 | 0 | 1 | 低电平0 | 偶 | 下降 |
| 2 | 1 | 0 | 高电平1 | 奇 | 下降 |
| 3 | 1 | 1 | 高电平1 | 偶 | 上升 |
- 常用的模式为0和3
- 每次传输数据的数据位没有限制
STM32的SPI特性及架构
STM32 的 SPI 外设可用作通讯的主机及从机,支持最高的 SCK 时钟频率为 fpclk/2 (STM32F103 型号的芯片默认 fpclk1 为 36MHz,fpclk2 为 72MHz),完全支持 SPI 协议的 4 种模式,一次传输数据帧长度可设置为 8 位或 16 位,可设置数据 MSB 先行或 LSB 先行。它还支持双线全双工 (前面小节说明的都是这种模式)、双线单向以及单线模式。其中双线单向模式可以同时使用 MOSI 及 MISO 数据线向一个方向传输数据,可以加快一倍的传输速度。而单线模式则可以减少硬件接线,当然这样速率会受到影响。我们只讲解双线全双工模式。
通讯引脚
- SPI3对应的几个引脚,默认功能是下载。如果想要这几个引脚作为SPI引脚,需要在软件层面先禁用掉。一般在资源不是十分紧张的情况下,这几个引脚不会复用为SPI
- 如果就是用了,那么要按着复位键,再点下载。按住复位时,SPI3这几个引脚就会保持默认下载的状态
- SPI1在APB2上,SPI2和3在APB1上,通讯速率不同
- 由于NSS引脚的功能很简单,一般用软件选择一个GPIO引脚
时钟控制逻辑
- SCK时钟信号,由波特率发生器根据CR1寄存器的BR位控制
- 控制CR1寄存器的CPHA和CPOL位可以控制SPI为前面的4种模式
数据控制逻辑
SPI的MOSI和MISO都连接在数据移位寄存器上。
- 发送数据:写SPI的数据寄存器DR,把数据填充到发送缓冲区中,数据移位寄存器再一位一位地把数据通过数据线发送出去
- 接收数据:数据移位寄存器把采样到的数据填充到接收缓冲区中,读数据寄存器DR,可以获取接收缓冲区的内容
整体控制逻辑
- 主要是控制寄存器CR1和CR2来控制,控制SPI模式、波特率、LSB先行、主从模式、单双向模式等
- 在工作时,可以读取状态寄存器SR来获得工作状态
- 控制逻辑还负责控制产生SPI中断信号、DMA请求、控制NSS信号
- 在实际应用中,一般不使用STM32的SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制其电平输出,从而产生起始和停止信号
STM32的SPI通讯过程
-
数据寄存器DR对应两个缓冲区:发送缓冲区、接收缓冲区
-
在发送第一个数据位时,数据字被并行地(通过内部总线)传入移位寄存器,而后串行地移出到MOSI引脚
- TXE=1:发送缓冲区为空,数据都转移到以为寄存器了
-
对于接收器来说,当数据传输完成时:传送移位寄存器里的数据到接收缓冲器,并且RXNE标志被置位。在最后采样时钟沿,RXNE位被设置1,在移位寄存器中接收到的数据字被传送到接收缓冲器
- RXNE=1:接收缓冲区非空,接收的数据全都到了接收缓冲区
-
发送和接收是同步进行的,也就是说:接收结束也对应着发送的结束
- SPI发送和接收函数可以是同一个
SPI初始化结构体
SPI相关寄存器
FLASH(W25Q64为例)
FLSAH 存储器又称闪存,它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH 存储器容量普遍大于 EEPROM,现在基本取代了它的地位。我们生活中常用的 U 盘、SD 卡、SSD 固态硬盘以及我们 STM32 芯片内部用于存储程序的设备,都是 FLASH 类型的存储器。在存储控制上,最主要的区别是 FLASH 芯片只能一大片一大片地擦写,而在“I2C 章节”中我们了解到 EEPROM可以单个字节擦写
FLASH的存储特性:
在写入数据前必须先擦除
擦除会把数据位重置为1
擦除时必须按照最小单位来擦除(一般为sector)
写入时只能把1改为0
norflsh: 可以按字节为单位读写
nanflash: 必须以block为单位读写
芯片引脚
- /表示低电平
- 【CS:NSS】【DO(device out):MISO】【DI(device in):MOSI】【WP:写保护】【HOLD:通讯暂停(一般不用)】
W25Q64内部的寄存器
- BUSY位说明了芯片是否在进行内部时序的操作,为0则表示空闲,1表示正在处理
CAN协议
(Controller Area Network)
CAN控制器局域网络,是德国BOSCH公司开发的,国际上应用最广的现场总线之一。已成为汽车计算机控制系统和嵌入式工业控制局域网的标准总线。
| 特性 | CAN | CAN FD |
|---|---|---|
| 数据传输速率 | 最高可达1Mbps | 理论上可达8Mbps或更高 |
| 数据帧长度 | 最多8个字节的数据 | 最多64个字节的数据 |
| 数据帧结构 | 标准结构 | 引入额外字段,如BRS和ESI |
| 远程帧 | 使用 | 不使用 |
| 复杂性和成本 | 相对较低 | 相对较高,因为技术更先进 |
| 向后兼容性 | 不适用 | 设计时考虑与现有的CAN网络的兼容性 |
| 应用场景 | 适合数据量不大的应用 | 适合数据量大和对传输速率要求高的应用场景 |
CAN硬件电路
CAN协议是异步通讯,只有CAN_High和CAN_Low两条信号线,共同构成一组差分信号线进行通信。
主要分为闭环总线和开环总线,前者用于高速通讯,后者适用远距离通讯。
闭环总线网络
高速、短距离,它的总线最大长度为 40m,通信速度最高为 125K-1Mbps,总线的两端各要求有一个120R的电阻。
闭环能让总线快速回归隐性电平,所以传输速度快
开环总线网络
低速、远距离“开环网络”,它的最大传输距离为 1km,最高通讯速率为10k - 125kbps,两根总线是独立的、不形成闭环,要求每根总线上各串联有一个2.2K的电阻
开环总线回归隐性电平速度慢,所以速率低
-
通讯节点:每个节点由一个CAN控制器(stm32芯片或者其他控制芯片)和一个CAN收发器组成,CAN_Rx和CAN_TX是TTL电平,CAN协议要求的是差分信号。通过CAN收发器,把TTL电平转成差分信号,发送到总线上;或者把总线上的差分信号,转换为TTL电平,发给控制器
-
差分信号:又称差模信号,需要两根信号线,振幅相等,相位相反,通过两根信号线的电压差((V+)-(V-))来表示逻辑0和逻辑1
- 抗干扰能力强。外界噪声会几乎同时耦合到两条信号线上,作差后最终的信号不变。所以外界的共模噪声可以被完全抵消
- 有效抑制它对外部的电磁干扰。两根信号线极性相反,对外辐射的电磁场可以相互抵消。耦合越紧密,泄放到外界的电磁能量越少。
- 时序定位精确。由于差分信号的开关变化是位于两个信号的交点,而不像普通单端信号依靠高低两个阈值电压判断,因而受工艺,温度的影响小,能降低时序上的误差,同时也更适合于低幅度信号的电路。
- 由于差分信号线具有这些优点,所以在 USB 协议、485 协议、以太网协议及 CAN 协议的物理层中,都使用了差分信号传输。
CAN通信中的差分信号:
一般都用高速
- 显隐性对应的是总线的状态。
- 显性电平-逻辑0-压差非0。
- 隐性电平-逻辑1-压差为0
- 在 CAN 总线中,必须使它处于隐性电平 (逻辑 1) 或显性电平 (逻辑 0) 中的其中一个状态。假如有两个 CAN 通讯节点,在同一时间,一个输出隐性电平,另一个输出显性电平,类似 I2C 总线的“线与”特性将使它处于显性电平状态,显性电平的名字就是这样来的,即可以认为显性具有优先的意味
- 由于 CAN 总线协议的物理层只有 1 对差分线,在一个时刻只能表示一个信号,所以对通讯节点来说,CAN 通讯是半双工的,收发数据需要分时进行。在 CAN 的通讯网络中,因为共用总线,在整个网络中同一时刻只能有一个通讯节点发送信号,其余的节点在该时刻都只能接收。
CAN总线帧格式
can协议定义的5中类型的帧。数据帧用的最多,主要学习
在发送一帧开始前,总线必须是隐性电平D
帧起始SOF:显性电平
报文ID:11位,不同功能的数据帧,其ID都不同,该ID越小,优先级越高
DTR:远程请求标志位,区分数据帧还是遥控帧
IDE:ID扩展标志位,区分是标准格式还是扩展格式
r0:保留位,必须显性0
DLC:表示数据段data的长度
Data:实际要传送的数据,1到8字节。数据段的位长度要是8的倍数
CRC:CRC校验用
FSMC
(Flexible static memory controller)
专门用于管理扩展的存储器的外设。static意味着只能管理静态的存储器SRAM、NOR FLASH、NAND FLASH,不能管理动态的存储器,比如SDRAM。
如果有FMC外设,可以管理SDRAM
2023.10.8
。。。。。太复杂了,板子还不支持,先跳过了。。。。。
ADC
功能框图
1.电压输入范围
VREF- ⇐ VIN ⇐ VREF+
决定输入电压的引脚:VREF-、VREF+、VDDA、VSSA。前两个是mcu的GPIO引脚,后两个是模拟电压的电源和模拟电压的地。总是把VREF-和VSSA接地,VREF+和VDDA接3.3V,所以ADC输入电压范围0-3.3V
超出范围的电压,只能通过外围电路,把低于范围的拉高,高于范围的拉低,再测。
根据KCL定律,可以求出粉色点处的电压与输入电压之间的关系
2.输入通道
输入通道又分为规则通道和注入通道
规则通道(最多4个):顾名思意,规则通道就是很规矩的意思,我们平时一般使用的就是这个通道,或者应该说我们用到的都是这个通道,没有什么特别要注意的可讲。
注入通道(最多16个):它是一种在规则通道转换的时候强行插入要转换的一种通道。如果在规则通道转换过程中,有注入通道插队,那么就要先转换完注入通道,等注入通道转换完成后,再回到规则通道的转换流程。这点跟中断程序很像,都是不安分的主。所以,注入通道只有在规则通道存在时才会出现
3.转换顺序
规则序列寄存器、注入序列寄存器分别控制
4.触发源
软件触发:ADC_CR2寄存器的ADON打开ADC,再控制SWSTART和JSWSTART位让ADC开始转换
外部事件触发: 来自内部定时器或外部GPIO
选择事件源:ADC_CR2寄存器的EXTSEL和JEXTSEL
允许转换:EXTTRIG和JEXTTRIG
5.转换时间
转换时间Tconv=采样时间+12.5个周期
采样周期:ADC需要若干个ADC_CLK周期完成对输入模拟量进行采样,采样周期数可以ADC_SIMPR1x寄存器控制每个通道的采样周期
ADC_CLK:ADC模拟电路时钟,有PCLK2提供,由RCC_CFGR寄存器控制(时钟树上有)
数字时钟:RCC_APB2ENR,用于访问寄存器
6.数据寄存器
ADC转换后的数据,规则组的放在ADC_DR寄存器,注入组放在ADC_JDRx寄存器
分辨率是12位,DR寄存器是32位分为高低16位,涉及到左对齐还是右对齐,ADC_CR2的ALIGN位控制
单通道时,可以直接读。多通道时,数据都会存到同一个地方,最好用DMA搬数据,因为不占用cpu。较少时还用中断读取。不建议连续采集且数据很多时用中断,因为会频繁进入中断,cpu一直在处理数据,没法做别的事情了
只能ADC1和3同时使用。ADC2不支持DMA功能
7.中断
转换结束、注入转换结束、模拟看门狗事件。模拟看门狗是检测读入的模拟电压是否超过阈值的
寄存器
初始化结构体
基本定时器TIM
定时器功能:定时、输出比较(输出PWM)、输入捕获(测得输入信号的脉冲宽度、频率等)、互补输出()
基本定时器的功能:定时(顾名思义,功能很基本)
通用定时器:没有互补输出
高级定时器:都有
F103:基本定时器TIM6/7,通用定时器TIM2/3/4/5,高级定时器TIM1/8
功能框图
-
基本功能:
- 16bit向上计数。
- 没有外部GPIO,是内部资源,只能用来定时。
- 时钟来自PCLK1,72MHz,可以实现1-65536分频就
计数器的时钟,可以来自多个时钟源,最常用的就是内部时钟internal clock,就是晶振提供的时钟,经过芯片内部各种分频倍频操作后,得到的时钟。
1时钟源:2控制器:3时基单元:
PSC预分频器和自动重装载寄存器在功能框图里都有黑色的阴影,说明他们都有影子寄存器。影子寄存器起到缓冲的作用:用户值→寄存器→影子寄存器→起作用。当在某个计时过程中,用户修改了ARR的值,若没有启动影子寄存器,则ARR的值立即改变,当前的计数CNT会到新设定的ARR,则本次计时就会被打乱,定时时间不可控;若启动了影子寄存器,则本次计数结束后,ARR的值才会发生改变,保证了本次定时是可控的(之前的定时时间),下一次定时才是新设定的ARR。
影子寄存器由TIMx_CR1:APRE位控制
定时时间的计算
F103的TIMxCLK最大为72MHz(一般都是跑满)。设定PSC=72-1,则
定时器时钟 = 72M/(PSC+1) = 1MHz,计数一次的时间为 1/1MHz
设定ARR = 1000-1,从0计数到ARR共计数1000次,则
定时周期T = 1000*(1/1MHz) = 1ms
初始化结构体
寄存器
阻塞延时和非阻塞延时
阻塞:main函数中的for函数形式的delay,整个系统都在等待,不利于多任务的运行
非阻塞延时的普通实现:
// main函数中
while(1){
if(延迟开启){
定时时间--(或者其他计数标志);
if(定时时间==0){
状态flag=1;
}
}
if(状态flag==1){
状态flag=0;
定时时间复位;
相应的操作()或者给一个状态flag;
}
}但是 如果多任务都有定时,则这种延时模式会根据任务数量的增加,导致延时越来越不精确。解决方法 采用定时器实现该非阻塞延时
/* 定时器中断函数 */
{
关定时器中断;
if(允许计时开关) //开关可以用定时时间来代替,只要定时时间不是0,那么就意味着需要定时
{
定时时间--;
if(定时时间==0)
{
flag = 1;
}
}
重设定时器时间;
开启定时器中断;
}
/* 主函数 */
{
if(flag == 1)
{
flag=0;
任务操作;
}
}该方法可以实现一个硬件定时器生成多个软件定时器,充分利用了资源。但是要注意,进入定时器的周期不能太短,要不然频繁进入中断,会影响主程序运行。
高级定时器TIM
TIM1和TIM8
功能:定时、输入捕获、输出比较、互补输出、断路输入
- 16bit的 上/下/两边 计数,还独有重复计数器RCR
- 4个GPIO,其中1-3有互补输出(时钟脉冲高低电平反相)
- 时钟来自PCLK2,72MHz,可实现1-65536分频
功能框图
-
1.时钟源:
- 内部时钟来自RCC,(多出了 外部时钟(模式1/2)、内部触发输入)
- 外部时钟模式1,来自GPIO,对应4个通道,由TIM_CCMRx控制…用的比较少
- 外部时钟模式2,来自ETR引脚…用的比较少
- 内部触发输入:使用一个定时器作为另一个定时器的预分频器。硬件上高级控制定时器和通用定时器在内部连接在一起,可以实现定时器同步或级联。…用的比较少
-
2.控制器:
-
3.时基单元:
-
4.输入捕获:测量输入信号的脉宽或者频率
-
5.输出比较:可以输出一定频率的占空比的PWM波
初始化结构体
定时器的初始化结构体有四个:TIM_TimeBaseInitTypeDef 配置时基 TIM_OCInitTypeDef输出比较用 TIM_ICInitTypeDef输入捕获用 TIM_BDTRInitTypeDef刹车/死区时间
PWM输出模式
由ARR自动重装载寄存器确定PWM的频率,捕获/比较寄存器CCRx的值 确定占空比。 CCRx的值和CNT的值一直在比较,输出高或低电平。
SDIO 安全数字输入/输出接口
(看的F429挑战者V1的教程)
SD 卡 (Secure Digital Memory Card) 在我们生活中已经非常普遍了,控制器对 SD 卡进行读写通信操作一般有两种通信接口可选,一种是 SPI 接口,另外一种就是 SDIO 接口
SD卡
一张SD卡包括 存储单元、存储单元接口、电源检测、卡及接口控制器、接口驱动器 5个部分储单元是存储数据部件,存储单元通过存储单元接口 与卡控制单元进行数据传输;电源检测单元保证SD卡工作在合适的电压下,如出现掉电或上状态时,它会使 控制单元和 存储单元接口复位;卡及接口控制单元控制SD卡的运行状态,它包括8个寄存器;接口驱动器控制SD卡引脚的输入输出
SD卡总共有8个寄存器,用于设定或表示SD卡信息。这些寄存器只能通过对应的命令访问。SDIO 定义了64个命令
SD卡用9pin通信,3根电源线、4根数据线、1根命令线、1根时钟线。(原理图上画的都是SD卡座,所以引脚数量会大于9个)(TF卡8个脚,把两个电源地合并了,尺寸小)
CLK线由SDIO主机即stm32产生时钟信号,上升沿有效。CMD命令控制线,SDIO主机和SD卡通过该组线发送命令/提供应答。D0-3数据线,传输读写数据,SD卡可将D0拉低表示忙状态。
SD 卡操作过程会使用两种不同频率的时钟同步数据,一个是识别卡阶段时钟频率 FOD,最高为 400kHz,另外一个是数据传输模式下时钟频率 FPP,默认最高为 25MHz,如果通过相关寄存器配置使 SDIO 工作在高速模式,此时数据传输模式最高频率为 50MHz。
SD卡操作模式切换
STM32 控制器对 SD 卡进行数据读写之前需要识别卡的种类:V1.0 标准卡、V2.0 标准卡、V2.0 高容量卡或者不被识别卡。
SD 卡系统 (包括主机和 SD 卡) 定义了两种操作模式:卡识别模式和数据传输模式。
在系统复位后,主机处于卡识别模式,寻找总线上可用的 SDIO 设备;同时,SD 卡也处于卡识别模式,直到被主机识别到。(即当 SD 卡接收到 SEND_RCA(CMD3) 命令后,SD 卡就会进入数据传输模式,而主机在总线上所有卡被识别后也进入数据传输模式)
SDIO总线(暂时没看)
ETH 以太网外设
它实际是一个通过 DMA 控制器进行介质访问控制 (MAC),它的功能就是实现 MAC 层的任务。(MAC层就是介质访问控制层)
借助以太网外设,STM32F4xx 控制器可以通过 ETH 外设按照 IEEE 802.3-2002 标准发送和接收 MAC 数据包。
ETH 支持两个工业标准接口介质独立接口(MII)和简化介质独立接口(RMII)用于与外部 PHY 芯片连接,MII 和 RMII 接口用于 MAC 数据包传输。ETH 还集成了站管理接口(SMI) 接口专门用于与外部 PHY 通信,用于访问 PHY 芯片寄存器,它由两根线组成,数据线 MDIO 和时钟线 MDC
物理层定义了以太网使用的传输介质、传输速度、数据编码方式和冲突检测机制,PHY 芯片是物理层功能实现的实体。
生活中常用 水晶头网线 + 水晶头插座 +PHY 组合构成了物理层
ETH 外设负责 MAC 数据包发送和接收。利用 DMA 从系统寄存器得到数据包数据内容,ETH 外设自动填充完成 MAC 数据包封装,然后通过 PHY 发送出去。在检测到有 MAC 数据包需要接收时,ETH 外设控制数据接收,并解封 MAC 数据包得到解封后数据通过 DMA 传输到系统寄存器内
MPU 存储保护单元
Memory Protect Unit。MPU其实是属于ARM 某些内核(M3 M4 M7)自带的一个外设,是跟核绑定在一起的,所以关于它的资料都在ARM核的各种参考指南中。
MPU说简单点其实就是一个内存访问权限控制器,如果CPU在访问内存时不符合MPU定义的访问权限的话,那么访问就会被驳回,并且会触发一次错误异常,即Hardfault异常(或者MemManage异常,可通过SCB→SHCSR来配置)。配置内存访问权限的好处主要有:
- 避免应用程序破坏其他任务或者OS使用的栈和堆
- 避免非特权任务访问对系统可靠性和安全性很重要的外设
- 防止恶意代码注入攻击
- 控制存储器相关访问属性
MPU相关概念
MPU_mpu为什么不能直接访问寄存器1
MPU详解及其应用2
- Memory Map:stm32,能够访问 0 - 232-1 个地址范围,共4G大小的空间。芯片厂商会根据自己的设计将内部Flash,内部SRAM,TCM,外设寄存器,还有外部存储器等等的访问地址映射分布在这4G中,这就被称为Memory Map。MPU管理的对象就是整个4G空间
- MPU Region:MPU可将整个4G分成若干个区域,然后对每个区域设置地址区间(起始地址和大小)和不同的访问权限来达到预期的保护目标。这些区域就是MPU Region。这些区域之间可以有重叠。
- Region优先级:数字越大,优先级越高。当两个region存在重叠部分,服从优先级高的region
- Background Region:当有些地址并没有分配在MPU Region里时,这些没被覆盖的区域就称为Background Region。MPU可单独配置对Background Region的属性为默认特权访问权限还是不可访问
MDK的编译过程和产生的文件类型讲解
Microcontroller Development kit
编译过程
机器码就是01组合而成的二进制数据,直接与硬件交互,CPU直接执行,但是大多数人基本看不懂,也不会写。 为了更好的编程,人们发明了汇编语言(ADD R1, R2这样的),用汇编语言写完后,由汇编器转成机器码,方便了人们编程。
buid时,先把.c文件和.s文件通过工具,编成.o文件,每个.o文件都对应.c或.
s文件,.o文件是分散的,没什么地址的关联。
通过链接器,将这些分散的.o文件和库文件,链接成映像文件.axf
最后通过格式转换器,转换成hex或bin文件,可以下载到单片机中
如果用cubemx生成的工程,在ARM-MDK/工程名 文件夹中可以看到这些文件,除了.o .axf这些文件外,还有一些其他的文件

程序的组成、存储、运行
编译程序后输出的信息:Program Size:Code=xx RO-data=xx RW-data=xx ZI-data=xx 编译后,应用程序中所有具有同一性质的数据、代码会被归到一个域 .text:相当于 RO code .constdata:相当于 RO data .bss:相当于 ZI data .data:相当于 RW data
| 区域名称 | 含义 | 存储内容 | 是否占用 Flash | 是否占用 RAM | 特性 | 也可能称为 |
|---|---|---|---|---|---|---|
| Code | 代码段 | 程序代码(指令) | ✅ | ❌ | 只读、固定大小 | .text |
| RO-data | 只读数据段 | 常量(如const变量、字符串字面量“hello”) | ✅ | ❌ | 只读、固定大小 | .constdata |
| RW-data | 读写数据段 | 初始化为非 0 的全局 / 静态变量 | ✅(初始值) | ✅(运行时) | 可读写、需初始化 | .data |
| ZI-data | 零初始化数据段(BSS) | 未初始化或初始化为 0 的全局 / 静态变量 | ❌ | ✅ | 自动清零、不占 Flash | .bss |
| Stack | 栈 | 局部变量、函数调用上下文 | ❌ | ✅ | 自动管理、后进先出 | |
| Heap | 堆 | 动态分配的内存(如malloc) | ❌ | ✅ | 手动管理、灵活分配 | |
编译器给出的 ZI-data 占用的空间值中包含了stack和heap的大小 (经实际测试,若程序中完全没有使用 malloc 动态申请堆空间,编译器会优化,不把heap空间计算在内) | ||||||
| 程序的存储和运行: | ||||||
![]() |
选择完芯片后,会有芯片的Flash和SRAM,编写完的程序不能超过这些存储器的大小,如果超出了,编译器会提示错误,我们就可以根据不同数据存储的位置,进行代码裁剪
| 程序的状态 | 组成 | |
|---|---|---|
| 程序静止存储时 | Code+RO data+RW data | ⇐Flash |
| 程序执行的RW区域 | RW data + ZI data | ⇐RAM |
| 程序执行的RO区域 | RO data + code |
各个文件
.crf:交叉引用文件,按F12跳转时通过这个文件查找。在output选项里browse information才会生成该文件。当文件多时,取消勾选这个会加速编译速度 .htm:链接器生成的静态调用图文件。包含了整个工程各函数互相调用关系,给出了静态占用最深的栈空间数量以及它对应的调用关系链。 利用这些信息,我们可以大致了解工程中应该分配多少空间给栈
map文件
Section:描述映像文件的代码或数据块,我们简称程序段
节区的跨文件引用 Section Cross Reference:
74hc595.o(i.HC595_Init) refers to 74hc595.o(i.HC595_Send_Byte) for HC595_Send_Byte74hc595.o文件的i.HC595_Init节区 为它使用的HC595_Send_Byte符号,引用了74hc595.o文件的i.HC595_Send_Byte节区
hf-a21.o(i.hf_config) refers to usart.o(.bss) for huart6hf-a21.o文件的i.hf_config节区 引用了 usart.o文件 .bss段中的 huart6
删除无用节区 Removing Unused input sections from the image:
这部分列出了在链接过程它发现工程中未被引用的节区,这些未被引用的节区将会被删除 (指不加入到.axf 文件,不是指在.o 文件删除),这样可以防止这些无用数据占用程序空间。
符号映像表 Image Symbol Table: 比较重要
这个表列出了被引用的各个符号在存储器中的具体地址、类型、占据的空间大小 等信息

存储器映像索引 Memory Map of the image:
相对于符号映像表,这个索引表描述的单位是节区,而且它描述的主要信息中包含了节区的类型、属性,由此可以区分 Code、RO-data、RW-data 及 ZI-data
映像组件大小的信息 Image component sizes:
这部分包含了各个使用到的.o 文件的空间汇总信息、整个工程的空间汇总信息以及占用不同类型存储器的空间汇总信息,它们分类描述了具体占据的 Code、RO-data、RW-data 及 ZI-data 的大
小,并根据这些大小统计出占据的 ROM 总空间

sct文件
当工程按默认配置构建时,MDK 会根据我们选择的芯片型号,获知芯片的内部 FLASH 及内部SRAM 存储器概况,生成一个以工程名命名的后缀为.sct 的分散加载文件 (Linker Control File,
scatter loading),链接器根据该文件的配置分配各个节区地址,生成分散加载代码,因此我们通过修改该文件可以定制具体节区的存储位置
上面这段sct的作用是:程序的加载域为内部 FLASH 的 0x08000000,最大空间为 0x00100000;程序的执行基地址与加载基地址相同,其中 RESET 节区定义的向量表要存储在内部 FLASH 的首地址,且所有 o 文件及 lib 文件的 RO 属性内容都存储在内部 FLASH 中;程序执行时 RW 及 ZI 区域都存储在以 0x20000000 为基地址,大小为 0x00020000 的空间 (128KB),这部分正好是 STM32 内部主 SRAM 的大小
🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴
编码相关
统一编码 Unicode
Unicode又叫统一码、万国码、单一码,是一种在计算机上使用的字符编码。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。没有规定一个字符是几个字节来存储。
UTF-8
UTF-8 是 Unicode 的实现方式之一。
UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8 的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为
0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。2)对于
n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
sizeof()、strlen()
char s[] = "1234";
// 输出得到结果
sizeof(s) = 5 // 除了我们看到的1234,还有一个看不到的结束空字符'\0'
strlen(s) = 4 // 只统计字符长度
// 需要注意的是,strlen() 函数只能用于计算以空字符 '\0' 结尾的字符串的长度,如果字符串中没有空字符,则 strlen() 函数的行为是未定义的。硬件知识
BUCK电路
bilibili-直流电12V变5V 开关电源的工作原理 DC DC降压稳压电路的基本原理
BUCK电路是直流转直流的降压电路
电容
硬件基础元器件【2.电容篇】_电容等效模型3
电容C
C = Q/U = εS/4πkd
容抗Xc = 1/(2πfC),其中f为交流电频率,C是电容大小。由此可见,电容越大、交流电频率越高,容抗越小,隔直通交效果越好
在普通电路设计中,通常只需要关注电容的容值、精度、耐压值、封装、工作温度、温漂等参数。但是,在高速电路或者电源系统中,以及一些对电容要求很高的时钟电路中,需要考虑电容的各种寄生参数。此时的电容是由一个等效串联电阻ESR、等效串联电感ESL和一个等效并联电阻Rleak组成的电路。
对电容器件而言,由于电容分量的存在,电容器件的阻抗随着频率的升高而逐渐降低,这是电容器件的本体属性;ESL分量则使电容的阻抗随着频率的升高而逐渐增加。这两种作用正好相反。
实际情况,电容的阻抗并不是单调的,先是随着输入频率的上升,电容分量起主导作用,阻抗逐渐变小,体现出电容的阻抗特性。过了一定的频率后,电容分量和ESL分量的对抗效果抵消,阻抗达到最小,该点为谐振点。之后,ESL分量起主导作用,阻抗逐渐增大,表现为电感的阻抗特性。
由谐振频率公式 F = (ESR x C)(-1/2)可知,容值和ESL越大,则谐振频率越低
寄生电容
电容就是两个金属板,中间用绝缘物质隔开。更宽泛的考虑,PCB上两根并排的导线,中间被绝缘物质隔开,那不也就相当于一个电容了?
退耦电容
电源输出一般会接滤波电容(旁路bypass电容),但是导线本身会有寄生电阻、寄生电感,不同IC工作时电流也在变化,就会导致电源线上的电压波动,退耦电容接在IC电源输入引脚附近,就可以一定程度上滤除这些干扰。
其实退耦电容和旁路电容,都是为了滤除干扰
电源符号
VCC、VDD、VSS等是什么意思4
VCC:C=circuit 表示电路的意思, 即接入电路的电压;VDD:D=device 表示器件的意思, 即器件内部的工作电压;VSS:S=series 表示公共连接的意思,通常指电路公共接地端电压;
有些器件带VCC和VDD,说明他有电压转换功能
在场效应管中(或COMS器件),VDD为漏极,VSS为源极,VDD和VSS指的是元件引脚,而不表示供电电压。
PCB的一些孔
三极管
参考模拟电路的笔记5
场效应三极管
利用电场效应来控制电流大小的半导体器件
金属氧化物半导体场效应管(MOS FET)
G的电压大于或者小于S的电压,S和D之间导通。把G当成一个开关就行
P-MOS:Ug<Us导通,=截止
N-MOS:Ug>Us导通,=截止
结型场效应管(JFET)
单片机电路里见不太到
晶振
石英晶体的压电效应,当晶片外加一个变化电场时,晶体会产生机械形变;当极板间施加机械力,晶体内会产生交变电场。
把石英切成音叉的形状,施加交流电,就会产生机械振动,产生机械振动同时也会产生振动电压(尽管这种交变电场的电压极其微弱,但其振动频率是十分稳定的)。把这样的石英晶体封装起来,就是晶振。
把晶振放在振荡电路中,经典的皮尔斯振荡电路。电路通电后,电路会产生很多无规则噪声,被放大器放大,被送到晶振。其中频率与晶振谐振频率 相等的信号通过晶振,再送到放大器放大,最终产生稳定的频率(晶振起振的过程)。
按键消抖
抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。
硬件消抖
加RS触发器或者电容
给按键并联一个0.1uf的电容
软件消抖
(1)延时函数
当检测到按下时,延时一会,若果再检测到是按下,则进行按键处理。当检测到释放时,延时一会,若果再检测到是释放,则进行释放处理。
优点:简单容易操作
缺点:浪费CPU资源且延时不精准。且不能在按键中断中进行。(不要在中断中delay,中断服务函数要求快进快出,delay可能会导致其他中断在delay时触发,从而屏蔽了此次按键检测)
(2)定时器延时,在中断中处理
// 配置好定时器的 定时时间 和 打开中断
void 按键中断函数()
{
开启定时器;
}
//....在执行其他任务
void 定时器中断()
{
关闭定时器中断;
if(按键按下)
{
执行相应的任务
}
打开定时器中断;
}(2)状态机
状态机按键消抖6
(3)基于时间戳的debouncing
qmk固件里的方法
-
默认定义消抖时间 DEBOUNCE=5
定义 当前时间戳now,距离上次扫描的时间戳last_time,按键目前状态raw,按键上次状态last_raw,剩余消抖时间cnt
// 原始key状态数组 消抖后的key状态数组 多少个key 是否有key状态变化
bool debounce(matrix_row_t raw[], matrix_row_t cooked[], uint8_t num_rows, bool changed) {
uint16_t now = timer_read(); // 当前时间戳
uint16_t elapsed16 = TIMER_DIFF_16(now, last_time); // 上次扫描以来的时间间隔,last_time在最初的init时获取了一次
last_time = now;
uint8_t elapsed = (elapsed16 > 255) ? 255 : elapsed16; // 如果间隔超过255最大消抖时间则截断
bool cooked_changed = false;
uint8_t* countdown = countdowns;
// 扫描每个按键
for (uint8_t row = 0; row < num_rows; ++row, ++countdown)
{
matrix_row_t raw_row = raw[row];
if (raw_row != last_raw[row]) // 目前的状态和上次的状态不同,则开启消抖
{
*countdown = DEBOUNCE; // 剩余消抖时间countdown
last_raw[row] = raw_row;
}
else if (*countdown > elapsed) // 如果 前后两次状态相同 且 (剩余消抖时间 大于 上次扫描的时间间隔)
{
*countdown -= elapsed; // 消抖时间减少
}
else if (*countdown) // 如果 前后两次状态相同 且 (剩余消抖时间 <= 两次扫描间隔时间)
{
cooked_changed |= cooked[row] ^ raw_row; // ^操作,有1则1
cooked[row] = raw_row; // 消抖结束状态赋值
*countdown = 0; // 待消抖时间清除
}
}
return cooked_changed;
}
-----------------------------------------------
now = timer_read(); // 获取now时间戳;
elapsed = timer_diff(now, last_time); // 上次扫描和现在的时间间隔
last_ime = now; //更新上次扫描时间
if(raw != last_raw) // 目前状态 与 上次状态 不一致
{
cnt = DEBOUNCE; // 开始递减
last_raw = raw; // 记录状态
}
else if(elapsed < cnt) // 如果 前后状态一致 且 前后两次扫描时间 小于 目前剩下的消抖时间,说明还需继续消抖
{
cnt -= elapsed;
}
else if(cnt) // 如果 前后状态一致、且间隔时间大于等于剩余消抖时间
{
cnt = 0;
// 按键状态改变
}2.通用时间戳
// changed:是否有按键状态发生变化
bool debounce(matrix_row_t raw[], matrix_row_t cooked[], uint8_t num_rows, bool changed) {
bool cooked_changed = false;
if (changed) // 如果有变化,就把debouncing状态为true,获取这一时刻的时间戳
{
debouncing = true;
debouncing_time = timer_read_fast();
}
// 再次扫描时,若按键正处于消抖状态 且 距离开启消抖状态到现在的时间 >= 定义的消抖时间,则判定为完成消抖
else if (debouncing && timer_elapsed_fast(debouncing_time) >= DEBOUNCE)
{
size_t matrix_size = num_rows * sizeof(matrix_row_t);
if (memcmp(cooked, raw, matrix_size) != 0) // 检测按键状态是否变化
{
memcpy(cooked, raw, matrix_size);
cooked_changed = true;
}
debouncing = false; // 关闭消抖状态
}
return cooked_changed;
}
MSB/LSB
MSB: Most Significant Bit 表示最高位
LSB: Least Significant Bit 表示最低位
函数传入数组
void fun(int arry[])
{
//传进来的就是数组a的首地址
// sizeof(a) == sizeof(int*);
}
void fun(int* arry)
{
//和上面的一样
// 可以用a[]进行操作
}
实际上 int arry[] int []
int *arry int *
在函数声明里是等价的,传入的都是数组的首地址static修饰
static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁
修饰全局变量的时候,会使该变量的作用域限制在它所在的文件内,可以被任何方法和函数调用
static变量在程序中只初始化一次,即使被多个函数调用,其值也不会重置
在同一个c文件中,被static修饰的函数只能被同文件下的函数调用
extern修饰变量
用extern定义的变量表示,该变量在其他文件中定义过了。
boot-loader
使用串口下载程序_stm32串口下载7
引导加载程序、引导/启动管理器,引导计算机系统启动的。比如BIOS、uboot、GRUB等
boot-loader种类很多
串口下载中用到了bootloader
下载方式
程序的烧录方式 与 ISP一键下载8
仿真器:老版本的单片机,程序下载进去之后,单片机不能一步一步的运行。电脑连接仿真器,仿真器连接单片机,程序下载到仿真器中,不下载到单片机中。仿真器实时给单片机发送信号,实现一步一步运行的效果。后来cpu升级了,可以程序下载进去以后就一步一步运行(JTAG)
调试debug器:cpu里面实现一步一步运行的东西
stm32有两种调试接口:
JTAG接口:20pin
SWD接口:4pin
常用的调试器:
JLink:
STLink:
定义新类型
#define INTP int*
INTP i,j; // 实际上是 int *i,j; 只有i是指针,而j还是int型变量
typedef int* INTP;函数指针
函数的指针:指向函数的指针变量。
int find_max(int x, int y)
{
return x > y ? x : y;
}
// 用 (*标识符)代替函数名,剩下照抄,形参名可以不需要
int main(void)
{
/* p 是函数指针,用函数名,给指针赋值 */
int (* p)(int, int) = & find_max; // &可以省略; 也可以这么写 int (*p)(int, int) = NULL; p = find_max;
int a, b, c, d;
printf("请输入三个数字:");
scanf("%d %d %d", & a, & b, & c);
/* 与直接调用函数等价,d = find_max(find_max(a, b), c) */
// 调用方式1:直接用 函数指针 替换 函数名
d = find_max(find_max(a, b), c); // 正常调用
d = p(p(a, b), c); // 函数指针调用
(*d)((*d)(a,b),c); // 基础指针的方式调用
// 调用方式2:
printf("最大的数字是: %d\n", d);
return 0;
}
// 上面的代码只是演示了一下函数指针的作用,在实际开发中,基本不会这么写
// int *p(int, int); 如果(*p)不加括号,变成这样,则p先和(int,int)结合,再与*结合。这样就成了 返回值为指针的类型的函数。
// 直观点写就是 int* func(int,int); 返回值是int类型的指针函数指针有什么用
充当回调函数:以函数指针为参数的函数
多线程用的较多
int find_max(int a, int b) // 找大的值
{
return a > b ? a : b;
}
int find_min(int a, int b) // 找小的值
{
return a < b ? a : b;
}
void print_data(int (*d)(int, int), int a, int b) // 使用ab,完成某一函数功能
{
printf("%d\n", d(a,b));
}
int main()
{
void (*dd)(int (*)(int, int), int a, int b) = NULL;
dd = print_data;
// 由于传入的函数指针不同,导致功能不同
dd(find_max, a, b);
dd(find_min, a, b);
}还有就是用在软件架构分层时,下层函数要调用上层函数时,可以在下层函数c文件中,定义一个回调注册函数,其作用是将形参函数指针,赋值给下层c文件中的某一函数指针。这样在上层c文件中,调用这个回调注册函数后,下层c文件就可以调用上层的某函数了。
DFU固件升级
如果要做DFU下载,这里是不是要绕开,如果需要绕开,那这里应该有拨码开关切换到直连的USB接口上
启动模式切换就可以切到DFU。要么用DFU,要么用ISP,都可以
https://blog.csdn.net/m0_72615170/article/details/136385992
这个是用ISP方式的
把CH340直接放上板子就可以了
不是正好提出来要通过USB写上位机配置的吗,接到可以ISP的串口上就OK了
Footnotes
-
MPU_mpu为什么不能直接访问寄存器
- 深入理解MPU_mpu为什么不能直接访问寄存器-CSDN博客
- https://blog.csdn.net/qq_20334947/article/details/86603530
- 文章浏览阅读1.9w次,点赞25次,收藏190次。文章目录前言何为MPUMPU适用场景探讨MPU相关寄存器介绍前言之前在一段时间内接触过MPU,当时由于要完成任务,所以对MPU没有做过多的研究,并且在网上搜索关于MPU的资料,能把他介绍的很全面的很少,下面是我个人结合ARM的官方文档以及自己整理的一些资料,以Cortex-M0+架构为基础讲解MPU,希望能对大家有所帮助。何为MPUMPU意思是Memory Protect Unit,即为存…_mpu为什么不能直接访问寄存器
- 2024-09-28 14:38:18
文章目录
前言
之前在一段时间内接触过MPU,当时由于要完成任务,所以对MPU没有做过多的研究,并且在网上搜索关于MPU的资料,能把他介绍的很全面的很少,下面是我个人结合ARM的官方文档以及自己整理的一些资料,以Cortex-M0+架构为基础讲解MPU,希望能对大家有所帮助。
何为MPU
MPU意思是Memory Protect Unit,即为存储保护单元,它是位于存储器内部的一个可编程的区域,定义了存储器的属性和存储器的访问权限。MPU不会提升嵌入式应用的性能,而是用于系统中问题的检测(比如试图访问非法或者不允许的存储器位置所导致的应用错误)。如果检测到有错误,则会触发HardFault异常。实际上,许多微控制器用不到MPU,但MPU可以提高嵌入式系统的健壮性,在如下的情况中使得系统更加安全:
- 避免应用任务破坏其他任务或者OS内核使用的栈或数据存储器。
- 避免非特权任务访问对系统可靠性和安全性很重要的外设。
- 将SRAM或RAM定义为不可执行的(永不执行,XN),可以防止代码注入攻击。
还可以利用MPU定义其他存储器属性,例如可被输出到系统级的缓存单元或存储器 控制器的可缓存性。MPU默认是禁止的,此时对于存储器来说,其使用的是默认的存储器属性。
MPU适用场景探讨
其实对于简单的应用,比如IO控制,不太可能会用到MPU,除非使用的微处理器中存在系统级的缓存且需要MPU对其进行定义。
物联网,如果应用是和网络相关的东西,或者应用面临着无法信任的通信接口,则MPU有助于提高安全性。例如:将用于通信缓冲的存储器区域定义为不可执行的地址区域后,就可以防止代码注入攻击。
工业控制领域,如果应用需要很高的可靠性,则MPU可为多任务系统中栈加以限制,以检测一些意想不到的错误。
汽车应用,MPU常用于汽车部件中。软件部件间不能互相有接口,所以需要MPU处理存储器区域。我们可以将MPU的应用分为以下几类:
- 安全管理:
未受信任或者风险较高的软件部件,应该运行在非特权等级。MPU可以限制这些部件访问的存储器空间。
用作通信缓冲RAM空间中可能包含通信注入的恶意代码,MPU可以将存储器空间定义为不可执行。 - 系统可靠性:
在多任务的系统中,MPU可以定义应用任务栈的合法存储器空间,使得任务不会破坏其他任务或OS数据栈的空间。
若应用具有较高的安全需求,则MPU可以将栈空间最后定义为不可访问的存储器空间,这样可以检测出栈溢出。
有些程序可能需要将代码复制到SRAM中执行,或者将向量表复制到SRAM中以提高访问速度。在复制完代码或者向量之后,存储器空间可以被定义为只读的,防止存储器空间被意外的修改。 - 存储器属性管理:
可以利用MPU定义可被缓存的存储器空间,以及存储策略(写通vs写回)。
利用MPU配置覆盖掉某个存储器空间的默认配置。
总之,我们需要根据具体的项目需要,选择MPU是默认配置还是需要改一些配置,这样才能使我们的应用更加符合要求。
MPU相关寄存器介绍
Cortex-M0+处理器中的MPU最多支持8个可编程的存储器空间以及1个可选的背景区域。每个可编程的区域都有自己的起始地址、大小以及属性设置。对于ARM-v6M和ARM-v7M架构,MPU的区域可以重叠,
如果某存储区域位于两个已编程的MPU区域中,则其存储属性和权限会基于编号更大的那个区域,处理器在执行不可屏蔽中断(NMI)或HardFault处理时,MPU访问权限会被忽略。例如,可以将SRAM栈底的一小块SRAM空间定义为不可执行,将MPU用作栈溢出的检测机制。当栈到达边界的时候,HardFault可以忽略MPU限制并在错误的处理中使用预留的SRAM空间。
下面介绍MPU相关寄存器。
1.MPU类型寄存器
其可用于确定MPU是否存在,若DREGION区域读出为0,则表明MPU不存在。
(MPU→TYPE, 0xE000ED90)位 名称 类型 复位值 描述 23:16 IREGION R 0 本MPU支持的指令区域数,由于ARM-v6M架构使用统一的MPU,其总数为0 15:8 DREGION R 0或8 MPU支持区域数 0 SEPARATE R 0 由于MPU为统一的,其总为0 2.MPU控制寄存器
其有3个控制位,复位后,该寄存器数值为0,这样会禁止MPU。若要使能MPU,软件需要首先设置MPU区域,然后设置控制寄存器中的ENABLE位。
(MPU→CTRL, 0xE000ED94)位 名称 类型 复位值 描述 2 PRIVDEFENA R/W 0 特权等级默认的存储器映射使能,设置为1且MPU使能时,同背景区域一样,特权访问会使用默认的存储器映射,若未设置该位,则背景区域禁止且任何不在使能区域范围内的访问都将引发错误 1 HFNMIENA R/W 0 若设置为1,则MPU在硬件错误处理和不可屏蔽中断(NMI)处理中也是使能的,否则在硬件错误以及NMI中不使能 0 ENABLE R/W 0 若设置为1则使能MPU 说明:MPU控制寄存器中的PRIVDEFENA位用于使能
背景区(区域“-1”),若未设置其他区域,那么通过PRIVDEFENA,特权程序可以访问所有的存储器位置,只有非特权程序才会被阻止。如果设置并使能其他的MPU区域,则背景区域可能会被覆盖。
HFNMIENA用于定义NMI、HardFault异常执行期间或FAULTMASK置位时MPU的行为。MPU在这些情况下默认被禁止,即便是MPU设置的不正确,它也可以使HardFault和NMI异常处理正常执行。
3.MPU区域编号寄存器
在设置每个区域前,写入该区域以选择编程区域。
(MPU→RNR, 0xE000ED98)位 名称 类型 复位值 描述 7:0 REGION R/W – 选择待编程的区域 4.MPU区域基地址寄存器
每个区域的起始地址在MPU区域基地址寄存器中,利用VALID和REGION域,可以跳过设置MPU区域编号寄存器这一步,可以降低程序的代码复杂度。
(MPU→RBAR, 0xE000ED9C)位 名称 类型 复位值 描述 31:N ADDR R/W – 区域基地址,N取决于区域的大小,例如64KB大小区域的基地址为[31:16] 4 VALID R/W – 若为1,则bit[3:0]定义的REGION会用在编程阶段,否则会使用MPU区域编号寄存器选择的区域 3:0 REGION R/W – 若VALID为1,该域会覆盖MPU区域编号寄存器,否则会被忽略。由于Cortex-M3和Cortex-M4的MPU支持8个区域,当REGION域大于7时,会忽略掉区域编号覆盖。 5.MPU区域基本属性和大小寄存器
此外需要配置每个区域的属性。
(MPU→RASR, 0xE000EDA0)位 名称 类型 复位值 描述 31:29 保留 – – 28 XN R/W 0 指令访问禁止(1=禁止从本区域取指令,强行访问会引起存储器管理错误) 27 保留 – – 26:24 AP R/W 000 数据访问允许域 23:22 保留 – – 21:19 TEX R/W 000 类型展开域,在此架构中始终为0 18 S R/W – 可共用 17 C R/W – 可缓存 16 B R/W – 可缓冲 15:8 SRD R/W 0x00 子区域禁止 7:6 保留 – – 5:1 REGIO大小 R/W – MPU保护区域大小 0 ENABLE R/W 0 区域使能 其中REGION SIZE域决定了区域的大小,例如:
REGION大小 大小 b00111 256字节 b01000 512字节 b01010 2KB … … b11100 512MB … … b11111 4GB
子区域禁止用于将一个区域分为8个相等的子区域并定义每个部分为使能或禁止。若一个子区域被禁止且和另一区域重叠,则另一区域的访问规则会起作用。若子区域禁止但并未和其他区域重叠,则对该存储器区域访问会导致HardFault异常。
数据访问权限(AP)域(bit[26:24])定义了区域的AP,如下所示:AP数值 特权访问 用户访问 描述 000 无访问 无访问 无访问 001 R/W 无访问 只支持特权访问 010 R/W R 用户程序的写操作会导致异常 011 R/W R/W 全访问 100 无法预测 无法预测 无法预测 101 R 无访问 只支持特权读 110 R R R 111 R R R XN(永不执行)(bit[28])决定是否允许从该区域取指。
TEX(类型扩展)、S(可共享)、B(可缓冲)、C(可缓存)域(bit[21:16]),这些属性在每次寄存器和数据访问的时候都会被输出到总线系统,并且该信息可被写缓冲或缓存单元等总线系统使用。
而对于微控制器来说,只有B(可缓冲)属性会影响到处理器中的写缓冲。
若使用的微控制器支持设备缓存,则多数情况下可被配置为如下情况:类型 存储器类型 常用存储器操作 ROM,Flash(可编程存储器) 普通存储器 不可共用,写通,C=1,B=0,TEX=0,S=0 内部SRAM 普通寄存器 可共用,写通,C=1,B=0,TEX=0,S=0 外部RAM 普通寄存器 可共用,写回,C=1,B=1,TEX=0,S=1 外设 设备 可共用,设备,C=0,B=1,TEX=0,S=1 其中,
可共享属性对于具有缓存的多处理器系统非常重要,若传输标志为可用的,则缓存系统需要额外一些工作以确保不同的处理期间的缓存数据的一致性。单处理系统一般用不到可共享属性。MPU的使用和配置
在我们使用MPU之前,需要确定程序或应用程序访问的存储器区域,其中包括:
- 中断处理和OS内核在内的特权应用代码,一般来说
只支持特权访问。 - 中断处理和OS内核在内的特权应用使用的数据存储器,一般
只支持特权访问。 - 非特权应用程序代码,
全访问。 - 非特权应用栈等数据存储器,
全访问。 - 中断处理和OS内核在内的特权应用使用外设,
只支持特权访问。 - 用于非特权的外设,全访问。
对于存储器来说,MPU的要求为: - 存储器大小必须为2的整数次方,在256KB到4GB之间。
- 存储器起始地址必须对齐到区域大小的整数倍。例如我的区域大小为4KB(0x100),那么我的起始地址必须为
N*0x100
程序示例
这里我只写一些通用的可配置的一些子函数,具体如何配置还要根据不同的使用情况来,但大致都是这个流程。
//使能MPU时可以带有输入配置项 void mpu_enable(uint32_t options) { MPU->CTRL = MPU_CTRL_ENABLE_MASK | options; //禁止MPU __DSB(); //确保MPU设置生效 __ISB(); //利用更新后的设置 return; } //禁止MPU void mpu_disable(void) { __DMB(); //确保之前传输全部完成 MPU->CTRL = 0; //禁止MPU return; } //禁止区域函数(0到7) void mpu_region_disable(uint32_t region_num) { MPU->RNR = region_num; MPU->RBAR = 0; MPU->RASR = 0; return; } //使能区域函数 void mpu_region_config(uint32_t region_num, uint32_t addr, uint32_t size, uint32_t attributes) { MPU->RNR = region_num; MPU->RBAR = addr; MPU->RASR = size | attributes; return; }存储器屏障
在上述的代码示例中,我们使用到了存储器屏障指令:
-
DMB(数据存储器屏障),在禁止MPU前使用,确保数据的传输不会重新排序,并且如果有未完成的传输,会等到传输完成之后在写入MPU相应寄存器。 -
DSB(数据同步屏障),在使能MPU后使用,确保接下来的ISB指令只会在写入MPU控制寄存器结束后才执行,可以确保后续的数据传输使用新的MPU设置。 -
ISB(指令同步屏障),用于DSB之后,确保处理器流水线被清空且接下来指令利用更新后的MPU设置被重新读出。
建议使用上述指令,虽然处理器的流水线相对简单,忽略这些存储器特性也不会产生什么问题,但从软件可移植的角度来说,这样可以使软件可移植性得到提高。适用于其他Cortex-M系列处理器。
若MPU应用于嵌入式OS中,且MPU配置在上下文切换操作中完成(一般是PendSV异常处理),则异常入口和退出流程具有ISB的效果,所以从这个角度看就不需要ISB指令了。
后记
后续如果还有时间,可能会更新关于MPU子区域禁止的一些使用,但个人感觉这篇文章可以作为新手的入门了,希望能对你有所帮助,大家一起学习,共同进步! ↩
-
MPU详解及其应用
- 小猫爪:嵌入式小知识11-MPU详解及其应用-CSDN博客
- https://blog.csdn.net/Oushuwen/article/details/124712708
- 文章浏览阅读8.4k次,点赞21次,收藏126次。小猫爪:嵌入式小知识11-MPU详解及其应用1 前言2 MPU简介3 MPU相关概念3.1 Memory Map3.2 MPU Region3.3 Region优先级3.4 Background Region3.5 Cache的读写策略3.5.1 Cache的读操作3.5.1 Cache的写操作4 MPU寄存器介绍4.1 MPU_TYPE4.2 MPU_CTRL4.3 MPU_RNR4.4 MPU_RBAR4.5 MPU_RASR4.5.1 XN4.5.2 AP4.5.3 TEX, C, B, S4.5_mpu
- 2024-09-28 14:37:33
小猫爪:嵌入式小知识11-MPU详解及其应用
* [3.5.1 Cache的读操作](https://blog.csdn.net/Oushuwen/article/details/124712708#351_Cache_28)* [3.5.1 Cache的写操作](https://blog.csdn.net/Oushuwen/article/details/124712708#351_Cache_33)* [4.5.1 XN](https://blog.csdn.net/Oushuwen/article/details/124712708#451_XN_68)* [4.5.2 AP](https://blog.csdn.net/Oushuwen/article/details/124712708#452_AP_70) * [4.5.3 TEX, C, B, S](https://blog.csdn.net/Oushuwen/article/details/124712708#453_TEX_C_B_S_73) * [4.5.4 SRD](https://blog.csdn.net/Oushuwen/article/details/124712708#454_SRD_85) * [4.5.5 SIZE](https://blog.csdn.net/Oushuwen/article/details/124712708#455_SIZE_87)1 前言
前段时间被MPU(Memory Protection Unit)搞得头疼欲裂,所以就简单的学习了一下,得空做个总结,接下来就来看看这个MPU到底是个什么玩意?MPU其实是属于ARM核自带的一个外设,是跟核绑定在一起的,所以关于它的资料都在ARM核的各种参考指南中,大家请自行下载参考。
2 MPU简介
MPU说简单点其实就是一个内存访问权限控制器,如果CPU在访问内存时不符合MPU定义的访问权限的话,那么访问就会被驳回,并且会触发一次错误异常,即Hardfault异常(或者MemManage异常,可通过SCB→SHCSR来配置)。配置内存访问权限的好处主要有:
- 避免应用程序破坏其他任务或者OS使用的栈和堆
- 避免非特权任务访问对系统可靠性和安全性很重要的外设
- 防止恶意代码注入攻击
- 控制存储器相关访问属性
3 MPU相关概念
3.1 Memory Map
众所周知,大部分M核目前是32位寻址,那就代表了核能访问0~2^32-1地址范围,总共4G大小的内存空间。芯片厂商会根据自己的设计将内部Flash,内部SRAM,TCM,外设寄存器,还有外部存储器等等的访问地址映射分布在这4G中,这就被称为Memory Map,所以MPU管理的对象就是整个4G空间。
3.2 MPU Region
MPU可将整个4G分成若干个区域,然后对每个区域设置地址区间(起始地址和大小)和不同的访问权限来达到预期的保护目标。一般来说,M3核和M4最大支持8个区域,而M7最大支持16个区域,而这个区域就叫做MPU Region。
3.3 Region优先级
上面提到可对每一个MPU Region设置地址区间,那么如果两个Region有重叠的部分,那么这个时候Region Number大的优先级高。即对于同一块内存区间,Region0设置该区间可访问,而Region3设置该该区间不可访问,那么这个区间的权限遵循Region3的配置为不可访问。基本如下图所示:
3.4 Background Region
由于每个区间可设置地址区间,那么就会存在一种情况,那就是等所有的Region都配置完之后,4G空间中还有一些区域没有被所有的Region覆盖到,而这些没有被Region覆盖的区域就叫做Background Region。MPU可单独配置对Background Region的属性为默认特权访问权限还是不可访问。
3.5 Cache的读写策略
Cache是M核中的一个高速读写缓存区,在这里对其就不多做介绍了,在这里简单的介绍一下Cache的读写策略,另外Cache的读写策略被MPU控制。
3.5.1 Cache的读操作
如果CPU要读取的数据在Cache中已经加载好,这就叫读命中(Cache hit),如果Cache里面没有则就叫做读失效(Cache Miss)。如果Cache Miss后,就会有下列两种情况:
- 读通(Read through):直接从内存区读取数据,不经过Cache
- 读分配(Read allocate):先把数据读取到Cache中,再从Cache中读取数据
3.5.1 Cache的写操作
如果CPU要写的数据在Cache中已经开辟了对应的区域(专业词汇叫Cache Line,以32字节为单位),这就叫写命中(Cache hit),如果Cache里面没有开辟对应的区域,这就叫写失效(Cache Miss)。
如果如果Cache hit后,就会有下列两种情况:
- 写通(Write-through):在数据更新时,把数据同时写入Cache和存储区操作简单,写入速度慢
- 写回(Write-back):只有在数据被替换出Cache时,被修改的缓存数据才会被写到后端存储器,写入速度快,但是一旦Cache中更新的数据未被写入到存储器中去时出现断电,则数据会丢失
如果如果Cache Miss后,就会有下列两种情况:
- 写分配(Write allocate):先把要写的数据载入到Cache中,写Cache,然后再通过flush的方式写入到内存中。
- 写不分配(No-write allocate):直接把要写的数据写入到存储器中,不经过Cache。
4 MPU寄存器介绍
与MPU相关的寄存器也是非常的简单,如下:
4.1 MPU_TYPE

里面记录了MPU的最基础信息,那就是当前的这个MPU所能支持的Region个数(DREGION),一般可以通过读这个寄存器来了解当前片子是否支持MPU。4.2 MPU_CTRL

PRIVDEFENA:使能特权模式下的默认存储器映射使能,即Background Region在特权访问模式下的访问权限,1特权访问可正常访问Background Region;0禁止访问,一切访问都会触发错误异常。
HFNMIENA:MPU的规则在异常(NMI, HardFault,MemManage,BusFault等等)中是否起作用。1是,0否。4.3 MPU_RNR

决定当前配置哪一个Region,其中REGION决定Region的序号。在设置每个区域前,写入这个寄存器可以选择要配置的Region。4.4 MPU_RBAR

REGION:决定当前配置哪一个Region,其中REGION决定Region的序号。
VALID:决定MPU_RBAR[REGION]是否有效。1则有效;0则无效,这个时候MPU_RNR[REGION]有效。
ADDR:配置起始地址,起始地址必须要是Region大小的整数倍。仔细看ADDR所占的bit为[31:N],这个N是由Region大小决定。举个例子,如果设置Region的大小为64K(0x10000),那么起始地址必须为64K的倍数,N则为16,即[31:16]有效。4.5 MPU_RASR

这是MPU的核心寄存器了,其控制着每个Region的访问属性。4.5.1 XN
决定该Region是否可以取指令,类似于Linux中的可执行和不可执行。0可执行,1不可执行。
4.5.2 AP
决定这块区域针对特权模式和非特权模式的最基本的访问属性,具体如下:
4.5.3 TEX, C, B, S
在说TEX, C, B, S之前,先得介绍一下M核访问存储器数据的一个流程如下:

在图中可以看到MPU不仅控制核内的访问属性,还控制着核外的访问属性,总之及其复杂,而TEX(类型展开), C(可缓存), B(可缓冲), S(可共享)则就是控制这些和内核外的访问权限。其中S(可共享)决定在多核系统中该Region是否可被其他核访问;对于一般的M核控制器来说,它没有实现系统级缓存,所以对我们来说其实就一个写缓冲(B)需要我们去关心,即上面提到的cache读写策略,但是其它的属性也不能胡乱设置,总之复杂无比。系统级缓存又分为inner缓存和outer缓存两级缓存。上面提到一般M核控制器没有实现系统级缓存,那么inner缓存和outer缓存属性一致;如果控制器实现了系统级缓存,这两级缓存是可以被单独控制的,属性可不一致。
所以在设置TEX, C, B, S这几个属性的时候,需要将这4个属性结合在一起去考虑,具体如下表:

仔细看表中最后一项,当TEX最高位=1时,那么这个时候TEX的低两位(AA)决定outer缓存属性,而CB(BB)共同决定inner缓存属性,这适用于那些实现了系统级两级缓存的控制器。AA和BB具体如下:
4.5.4 SRD
子Region失能位。MPU将SIZE大于128字节的Region按照大小八等分分成8个子Region,而SRD总共8bit,则依次对应这8个子Region,可以通过SRD分被对这8个Region进行控制。0使能,1失能。
4.5.5 SIZE
决定了该Region的大小,具体请参考如下代码:
#define ARM_MPU_REGION_SIZE_32B ((uint8_t)0x04U) ///!< MPU Region Size 32 Bytes #define ARM_MPU_REGION_SIZE_64B ((uint8_t)0x05U) ///!< MPU Region Size 64 Bytes #define ARM_MPU_REGION_SIZE_128B ((uint8_t)0x06U) ///!< MPU Region Size 128 Bytes #define ARM_MPU_REGION_SIZE_256B ((uint8_t)0x07U) ///!< MPU Region Size 256 Bytes #define ARM_MPU_REGION_SIZE_512B ((uint8_t)0x08U) ///!< MPU Region Size 512 Bytes #define ARM_MPU_REGION_SIZE_1KB ((uint8_t)0x09U) ///!< MPU Region Size 1 KByte #define ARM_MPU_REGION_SIZE_2KB ((uint8_t)0x0AU) ///!< MPU Region Size 2 KBytes #define ARM_MPU_REGION_SIZE_4KB ((uint8_t)0x0BU) ///!< MPU Region Size 4 KBytes #define ARM_MPU_REGION_SIZE_8KB ((uint8_t)0x0CU) ///!< MPU Region Size 8 KBytes #define ARM_MPU_REGION_SIZE_16KB ((uint8_t)0x0DU) ///!< MPU Region Size 16 KBytes #define ARM_MPU_REGION_SIZE_32KB ((uint8_t)0x0EU) ///!< MPU Region Size 32 KBytes #define ARM_MPU_REGION_SIZE_64KB ((uint8_t)0x0FU) ///!< MPU Region Size 64 KBytes #define ARM_MPU_REGION_SIZE_128KB ((uint8_t)0x10U) ///!< MPU Region Size 128 KBytes #define ARM_MPU_REGION_SIZE_256KB ((uint8_t)0x11U) ///!< MPU Region Size 256 KBytes #define ARM_MPU_REGION_SIZE_512KB ((uint8_t)0x12U) ///!< MPU Region Size 512 KBytes #define ARM_MPU_REGION_SIZE_1MB ((uint8_t)0x13U) ///!< MPU Region Size 1 MByte #define ARM_MPU_REGION_SIZE_2MB ((uint8_t)0x14U) ///!< MPU Region Size 2 MBytes #define ARM_MPU_REGION_SIZE_4MB ((uint8_t)0x15U) ///!< MPU Region Size 4 MBytes #define ARM_MPU_REGION_SIZE_8MB ((uint8_t)0x16U) ///!< MPU Region Size 8 MBytes #define ARM_MPU_REGION_SIZE_16MB ((uint8_t)0x17U) ///!< MPU Region Size 16 MBytes #define ARM_MPU_REGION_SIZE_32MB ((uint8_t)0x18U) ///!< MPU Region Size 32 MBytes #define ARM_MPU_REGION_SIZE_64MB ((uint8_t)0x19U) ///!< MPU Region Size 64 MBytes #define ARM_MPU_REGION_SIZE_128MB ((uint8_t)0x1AU) ///!< MPU Region Size 128 MBytes #define ARM_MPU_REGION_SIZE_256MB ((uint8_t)0x1BU) ///!< MPU Region Size 256 MBytes #define ARM_MPU_REGION_SIZE_512MB ((uint8_t)0x1CU) ///!< MPU Region Size 512 MBytes #define ARM_MPU_REGION_SIZE_1GB ((uint8_t)0x1DU) ///!< MPU Region Size 1 GByte #define ARM_MPU_REGION_SIZE_2GB ((uint8_t)0x1EU) ///!< MPU Region Size 2 GBytes #define ARM_MPU_REGION_SIZE_4GB ((uint8_t)0x1FU) ///!< MPU Region Size 4 GBytes5 MPU应用注意事项
下面列出一些MPU的一些使用注意事项,如下:
- Region的起始地址必须是SIZE的整数倍
- 下面根据一些常见的存储器属性列出TEX, C, B, S的配置:
- MPU_CTRL[ENABLE]可使能和失能MPU,正确配置MPU流程为:失能MPU,配置Region属性,使能MPU。
- 在配置MPU过程中需要使用存储器屏障指令:DMB(数据存储器屏障),在禁止MPU前使用,确保数据的传输不会重新排序,并且如果有未完成的传输,会等到传输完成之后在写入MPU相应寄存器。DSB(数据同步屏障),在使能MPU后使用,确保接下来的ISB指令只会在写入MPU控制寄存器结束后才执行,可以确保后续的数据传输使用新的MPU设置。ISB(指令同步屏障),用于DSB之后,确保处理器流水线被清空且接下来指令利用更新后的MPU设置被重新读出。
- 在实际调试过程中,如果出现问题,可查看SCB寄存器组的CFSR寄存器和MMFAR查看MPU错误标志和触发MPU错误的地址:
6 MPU的errata和Workaround
在ARM官网中,查看相关errata文件,会发现有几条跟MPU相关的errata,就以M7为例,对应文件《Cortex-M7 (AT610) and Cortex-M7 with FPU (AT611) Product Revision r0p1 Software Developers Errata Notice》,每一条errata都标明了有此errata的core版本已经已经在哪一版已被修复。
6.1 1259864

这一条的大概意思就是M7在执行cache的写通操作时,如果满足一定条件并满足一定时序则会偶尔发生写通操作时序被打断导致最后数据不正确。该errata暂时没有直接的Workaround,可以通过将该Region的缓冲属性设置成写回,或者将其设置成不可缓冲(no-cacheable)。6.2 1013783

这一条的大概意思就是如果使能了MPU,并使能了PRIVDEFENA位,那么如果执行PLD指令去访问了那些没有被MPU Region覆盖的地址区域的话,可能会导致触发异常(HardFault或者MemManage)。另外M核的Speculative Access可能就会触发一个PLD指令,所以当存在这个bug的时候就会出现一个非常离奇的现象,那就是程序跑着跑着突然就挂了,而且这个挂死的程序位置还在不停的变化。这条errata的Workaround也是及其的简单,那就是让MPU的Region覆盖整个4G空间,并且将未被使用的区域的属性设置成no-access。具体做法就是让Region0覆盖整个4G区间,并且配置其AP属性为no-access,再配置其他的Region配置其他已被使用的内存空间,因为Region的优先级特性,后面的Region会覆盖掉Region0的属性。具体操作如下:
插入个小经历:之前接触RT1170的时候遇到了一个bug,就是程序跑着跑着程序就挂了,挂了之后,也没有进入异常,调试器连接不上核。后来经过一大堆人的努力,最后就查到了M核的这个errata,而这个errata出现错误是会触发HardFault的,而在RT1170上的现象并不一样。之所以没有触发异常,而是直接挂死,这跟RT1170本身的存储器控制器结构有关,RT1170去访问一个未被使用的一些特定区域时,RT1170是不会触发异常的,而是会把总线直接卡死。所以对这个现象所能给出的推测是:Speculative Access触发了PLD指令,因为被访问目的地址所在Region的访问属性被配置为可访问(比如这个区域处于在Background Region中,或者用户人为将其属性配置成可访问),所以PLD指令就会偶发的去访问该Region,而该Region在RT1170中正好又是未被使用特殊的区域(如未使用外部SDRAM),所以就会导致RT1170内部总线卡死。 (其实至今也没办法证明确实就是这个问题导致的,但是目前只有这一个合理的解释,而且这样修改后,死机问题也没有再次出现了)。解决办法其实跟上述一致,那就是让MPU的Region覆盖整个4G空间,并且将未被使用的区域的属性设置成no-access,具体操作方法同上所述。下面贴出RT1170的errata截图,上面描述了具体的描述和Workaround,感兴趣的人可以自行查阅。
6.3 834922

这一条的大概意思就是当HFNMIENA没有被使能时,即MPU的规则在异常中不生效,而且还要使用MPU的默认的Memory Map配置,这个时候去设置FAULTMASK的时候如果被打断了,那么接下来就会导致不可预估的事情发生(其实这个bug大概率是遇不到了,不仅产生条件苛刻,而且在r0p2版本早就修复了,不用太过担心)。这条errata的Workaround也是及其的简单,就是在置位FAULTMASK之前,先把PRIMASK置位,具体操作如下:
6.4 838169

这一条errata其实是cache的bug,跟MPU本身并没有多大的关系,只是说在配置MPU和cache存储之间如果没有DSB屏障指令的话可能会间接导致这个bug的发生。所以在这里再次强调,在配置MPU的时候,一定要按照5 MPU应用注意事项中的第四条的要求插入存储器屏障指令。6.5 834923

这一条的大概意思就是有一种情况可能会导致MPU工作不正常,由于导致这种情况出现的条件非常苛刻,而且也没有workaround去避免,所以只能做的就是尽量去避免这种情况发生,在这里就不多作解释和介绍了,具体的条件如下:
END
↩ -
硬件基础元器件【2.电容篇】_电容等效模型
- 硬件基础元器件【2.电容篇】_电容等效模型-CSDN博客
- https://blog.csdn.net/qq_43417647/article/details/128163045
- 文章浏览阅读1.4w次,点赞48次,收藏274次。电容是电子设备中不可缺少的电子元器件,应用十分广泛。电容的种类繁多,结构也各不相同,但其基本原理是一样的,都是依靠电荷的相互作用力把电荷存储起来。电容相比于电阻,种类更多,更加复杂。作为电子工程师,需要掌握各种电容的基本原理、基本参数、电气特性、选型方法等。_电容等效模型
- 2024-09-28 14:54:02
2 电容
文章目录
* [通用MLCC的分类](https://blog.csdn.net/qq_43417647/article/details/128163045#MLCC_116)* [MLCC选型要点](https://blog.csdn.net/qq_43417647/article/details/128163045#MLCC_127)* [去耦半径](https://blog.csdn.net/qq_43417647/article/details/128163045#_154)电容是电子设备中不可缺少的电子元器件,应用十分广泛。电容的种类繁多,结构也各不相同,但其基本原理是一样的,都是依靠电荷的相互作用力把电荷存储起来。电容相比于电阻,种类更多,更加复杂。作为电子工程师,需要掌握各种电容的基本原理、基本参数、电气特性、选型方法等。
2.1 电容的主要作用
- 作为电荷缓冲池
在电路中,电源的负载是动态的,即器件的电流和功耗是不断变化的。为了保证电路稳定工作,可以使用电容作为电荷的缓冲池,保证器件工作电压的稳定。(,表示电容两端电压变化量,表示电荷变化量,C为电容容值) - 用来泄放高频噪声
高速电路中,无时无刻都存在状态改变,从而在电路中产生大量噪声干扰。在电源的传输路径上,需要通过电容将这些高频噪声写放到相对稳定的地平面中,避免干扰器件的正常工作。(根据阻抗公式:,在频率较高时,电容表现为低阻抗) - 用于交流耦合
当两个器件通过高速信号相连时,信号想断的器件可能对直流分量有不同的要求。在这种情况下,需要使用电容将信号携带的直流分量在接收端之前滤除。
2.2 电容的主要参数
1、标称电容量
标称电容量为电容的标注值。其实际容值会随着工作频率、工作电压、测量方法等变化而变化。
2、额定电压
指在额定环境温度下,可以连续加在电容两端的最高直流电压有效值。
3、精度
实际容量与标称容量之间的偏差。
4、谐振频率
由于电容的寄生参数,在谐振点以上频率工作时,会表现为感性。应避免电容的工作频率高于谐振频率。
5、损耗因数
又称耗散系数,用字母表示。因为电容的泄露电阻、ESR、ESL比较难分开,所以许多厂家会将他们合并成一个指标,损耗因数。定义为:电容每周期损耗能量与存储能量之比,又称损耗角正切,表示为。在电容的泄露电阻、ESR、ESL三个指标中,ESR起主要作用,所以损耗因数计算时通常只考虑ESR值:
6、品质因数Q:电容的品质因数(Q值)为电容存储功率与损耗功率之比:
2.3 电容的等效模型
在普通电路设计中,通常只需要关注电容的容值、精度、耐压值、封装、工作温度、温漂等参数。但是,在高速电路或者电源系统中,以及一些对电容要求很高的时钟电路中,需要考虑电容的各种寄生参数。此时的电容是由一个等效串联电阻ESR、等效串联电感ESL和一个等效并联电阻Rleak组成的电路。
电容的等效模型如下图所示:
图2.1 电容等效模型 * ESL:由电容器的引脚电感和电容器两极间的等效电感串联组成
- ESR:由电容器件的引脚电阻和电容器件两极间等效电阻构成,主要取决于电容的工作温度、工作频率以及电容本身的导线电阻等
- Rleak:取决于电容的泄露特性
2.3.1 等效串联电阻ESR
电容的数据手册中给出的ESR一般为一定工作频率内,电容器内部串行电阻的最大值,该值随工作频率变化而变化。图2.2是YAGEO(国巨)公司的电容数据手册中,给出的阻抗和ESR随频率变化的曲线。图中的虚线为该电容在不同频率下的ESR值。
图2.2 国巨的电容阻抗频率特性曲线
同一封装下,容值越小,ESR越大;同一容值,封装越大,ESR越大。图2.3是muRata(村田)公司官网提供的电容ESR频率特性曲线(这是一个链接)。其中浅蓝色曲线是0603、0.1uF电容,红色曲线是0402、0.1uF电容,绿色曲线是0603、1uF电容,深蓝色曲线是0402、1uF电容。
图2.3 不同封装、容值的电容ESR频率特性曲线
电容的ESR往往会带来以下影响:
- 电容的ESR会产生损耗功率。
根据电容损耗角正切值的定义,较大的ESR会产生较大的损耗功率,虽然比较小,但电容数量较多时也需要考虑其功耗。 - 滤波电容中影响滤波效果
对于电源滤波电容来说,是通过给高频噪声一个低阻抗的对地回路,来实现滤除噪声的效果,ESR太大明显对滤波不利。 - 耦合电容中造成高频信号衰减
在交流耦合电路中,电容串接在信号两端,高ESR会对高频信号产生一定的衰减。
注意:通常来说,电容的ESR越小越好。但也存在例外情况,例如在LDO电源电路中,有时对输出电容有最低ESR要求,具体会在电源滤波电容章节叙述。
2.3.2 等效串联电感ESL
电容的ESL值通常取决于电容的类型和封装。需要考虑电容ESL的电路通常是高速电路,信号频率较高。这种情况下,往往采用ESL小的贴片电容,对于插孔式的电容,如铝电解电容,其ESL大得多。通常来说,贴片电容的封装越小,其ESL越小。这里的封装小,是指封装影响其寄生电感大小:内部电容体距PCB焊盘的距离更近,pcb焊盘与电容接触面积更大,则ESL更小。(0612封装的贴片电容与1206封装对比,0612封装电容的ESL小得多,甚至小于0201封装的电容)
2.3.3 电容阻抗的频率特性
如图2.4所示,对电容器件而言,由于电容分量的存在,电容器件的阻抗随着频率的升高而逐渐降低,这是电容器件的本体属性;ESL分量则使电容的阻抗随着频率的升高而逐渐增加。这两种作用正好相反。在电容分量和ESL分量的共同作用下,电容器件的整体阻抗表现为,随着频率的升高,首先是电容分量起主导作用,使阻抗逐步变小,器件表现为电容的阻抗特性,滤波效果渐强;当达到某一频点时发生谐振,此时电容分量和ESL分量对阻抗的效果正好抵消,在谐振点上,电容器件阻抗最小,等于电容的ESR分量;此后,随着频率继续升高,ESL分量起主导作用,使阻抗逐步增大,器件表现为电感的阻抗特性,滤波效果渐弱。
图2.4 电容阻抗频率特性曲线
滤波电容的作用机制是为噪声等干扰提供一条低阻抗回路,在噪声频率点上,要求滤波电容的阻抗较小,即当噪声频率落在谐振点附近时,电容的滤波效果最好。如图2.3所示,谐振点由两条曲线交会而成,左边的曲线取决于电容器件的容值C,右边的曲线取决于电容器件的ESL。由谐振频率公式可知,容值和ESL越大,则谐振频率越低,即电容对高频干扰的滤波效果越差;容值和ESL越小,谐振频率越高,越适于滤除高频干扰。
噪声等干扰的频率往往并不确定,高速电路中需要的是一个比较宽的低阻抗频带,满足电路滤除各频段噪声的滤波要求。理想电容的阻抗会随频率不断降低,但由于等效串联电感的存在,当频率增大到一定值时,阻抗反而会增大。也正是因为谐振频率的存在,我们可以利用不同型号电容谐振频率不一的特点,构筑一段低阻抗的频带,达到更好的滤波效果。因此,我们经常可以看到滤波电路中,采用了多种不同型号的的电容。
那么实际使用多个电容并联来滤波时,该如何确定各个电容的参数呢?
电路设计需要考虑高频和低频两种噪声,针对这两种噪声,应选取不同的滤波电容。低频噪声选用大电容,高频噪声选用小电容,这是许多工程师达成的共识。在实际工作中,这种说法并不完全正确。
在前面我们已经论述过,电容的阻抗随频率变化的曲线与他的寄生参数密切相关。其寄生参数对阻抗-频率特性曲线的具体影响如图2.5所示。当使用的电容的容值更大时,阻抗的低频特性会更好,低频阻抗会更低。当使用的电容ESL更小时,电容的高频阻抗会更小,拥有更好的高频性能。而电容的ESR则决定了电容的全频段阻抗,电容的ESR更小,则谐振点的阻抗会越低。在滤波时,选用电容的ESR自然是越小越好。
图2.5 电容阻抗频率特性曲线随寄生参数变化
电容的容值可以通过通过额定电容量来选择,而电容的ESL与封装大小相关。因此我们通过选用不同容值,不同封装的电容就可以实现想要的滤波效果。总的选取原则为低频噪声选用大封装大电容,高频噪声选用小封装小电容。例如图2.6中红色曲线和绿色曲线构成的滤波频带,是我们想要达成的效果。
图2.6是muRata(村田)公司官网提供的电容阻抗频率特性曲线。其中浅蓝色曲线是0603、0.1uF电容,红色曲线是0402、0.1uF电容,绿色曲线是0603、1uF电容,深蓝色曲线是0402、1uF电容。
图2.6 不同封装、容值的电容阻抗频率特性曲线
当同封装,不同容值电容并联时,电容的ESL基本相当,容值小的ESR更大;参考图2.6中浅蓝色和绿色曲线、深蓝色和红色曲线;可以看出小容值电容起不到我们想要的拓宽滤波频带的作用,只能降低滤波电路阻抗。同理,当同容值、不同封装时,也达不到想要的滤波效果。实际电源滤波电路中,也存在许多个同容值、同封装电容并联的情况,如图2.7所示。这样做的目的,一方面是起到去耦电容的本地“小池塘”作用,另一方面是为了在谐振点上得到更低的阻抗。需要说明的是,这样做,并没有展宽低阻抗频带。
图2.7 同容值、封装电容阻抗频率特性曲线
扩展补充
大电容与小电容的容值差距不能太小,太小不能构成足够宽的低阻抗滤波频带;也不能太大,大电容在高频带呈感性,可能会与小电容构成LC并联谐振电路,在谐振状态时,电容和电感之间进行周期性的能量交换,以至流经电源层的电流非常小,电源层表现为高阻抗状态,称这种现象为反谐振。
图2.8 电容并联阻抗频率特性曲线-反谐振
2.4 选型要点
在各种电容选型时,需要考虑的因素很多。除了标称容量、精度、额定电压、工作频率、封装尺寸、工作温度、阻抗(低频ESR、高频ESL)、纹波电流能力等基本参数外,不同型号、不同用途的电容还有一些需要特别注意的要点。
2.4.1 多层陶瓷电容(MLCC)
通用MLCC的分类
MLCC可分为I类电容(低电容率系列、顺电体)和II类电容(高电容率系列、铁电体)。
I类陶瓷电容为温度补偿类电容,其电气性能稳定,基本不随温度、电压、时间的变化而变化,但容量一般比较小。通常使用在对稳定性、可靠性要求高的电路中。温度补偿型陶瓷电容NP0(Negative Positive Zero)就是I类陶瓷电容,温度范围为-55 ~ +125,温度系数为。NP0采用的是美国军用标准(MIL)来命名的,当采用美国电子工业协会(EIA)的命名标准时,表示为C0G。
II类陶瓷电容就是我们常见的X7R、X5R、Y5V等。其温度稳定性较差,但容量相对较大。图2.8展示的是II类陶瓷电容命名符号的含义。
图2.8 MLCC的II类电容命名表
MLCC选型要点
- 容值降额至少20% 。电容的标称容值为工作电压0V、环境温度25时的值。在实际使用中,工作电压升高,电容的有效容值会降低;环境温度相对于25不管是升高还是降低,都会引起电容的有效容值下降。
- 优先选用X7R和X5R电容,高精度场景选用NP0介质电容。
- 关注电容的ESR参数,注意满足线性电源稳定性和ESR要求。
2.4.2 钽电容
钽电容使用金属钽作为介质,基于钽的固态特质,具有温度特性好、ESL值小、高频滤波性能好、体积小、容值较大等特点。因此钽电容一般被应用在需要大容量电容滤波的场合,如为CPU等高耗能器件滤波。钽电容的缺点是耐电压和耐电流的能力较弱。
一般要求钽电容的工作电压相对额定电压降额50%以上。遇到以下三种场合之一,钽电容的额定电压需降额70%以上使用:
- 负载呈现较强的感性
- 串联电阻小
- 瞬变电流大
其原因在于,感性负载或较小的串联电阻会导致较大的瞬变电流,造成钽电容的金属钽介质被击穿。这使得在以下环节,铝电容的失效概率增大:ICT测试、老化测试、系统开机瞬间、单板热插拔瞬间。
一般而言,容值越大的钽电容,其ESR值往往越小。根据电容的等效电路,ESR相当于电容器件的串联电阻,串联电阻越小越容易造成钽电容失效。因此在应用中需要注意,对于大容值的钽电容,更需要电压降额。从成本上来说,在使用大容值的钮电容时,还需要增加电压降额的比例,这势必造成成本的上升。因而在设计中,往往将若干小容值的钮电容并联以提供和大容值钮电容相同的容量。
钽电容本身具有一定的自愈能力,钽电容失效后不一定永久失效,只要外界环境的影响在一定范围之内,钽电容都能自我恢复。因此,为了保证单板长期稳定的工作,必须严格执行钮电容的电压降额,同时在设计时需注意,在涉及热插拔的电源滤波电路上,尽量避免使用钽电容。
2.4.3 电解电容
优点是容量大、耐压高;缺点是精度差、温度稳定性差、高频特性差(ESL大、谐振频率低)。选用时,电压降额至少20% 。
2.5 电容的主要应用场景
2.5.1 去耦电容
去耦电容,其作用是为保证器件稳定工作而给器件电源提供的本地“小池塘”。在高速运行的器件上,会不断产生快速变化的电荷需求,对于这种快速的需求,电源模块无法及时给器件提供电流以补充,只能依靠器件附近的电容给予解决。去耦电容还有另一个作用,是为高速运行器件产生的高频噪声提供一条就近流入地平面的低阻抗路径,避免这些噪声干扰影响到该电源的其他负载。
去耦半径
理解去耦半径最好的办法就是考察噪声源和电容补偿电流之间的相位关系。当芯片对电流的需求发生变化时,会在电源平面的一个很小的局部区域内产生电压扰动,电容要补偿这一电流(或电压),就必须先感知到这个电压扰动。信号在介质中传播需要一定的时间,因此从发生局部电压扰动到电容感知到这一扰动之间有一个时间延迟。同样,电容的补偿电流到达扰动区也需要一个延迟。因此必然造成噪声源和电容补偿电流之间的相位上的不一致。
电容,对与它自谐振频率相同的噪声补偿效果最好,我们以这个频率来衡量这种相位关系。设自谐振频率为,对应波长为,补偿电流表达式可写为:
其中,是电流幅度,是需要补偿的区域到电容的距离,为信号传输速度。
当扰动到电容的距离达到**时,相位相差 ,补偿电流达到扰动点的相位为 ,即完全反向,去耦电容失去作用。此时补偿电流不再起作用,补偿的能量无法及时送达。为了能有效传递补偿能量,应使噪声源和补偿电流的相位差尽可能的小,最好是同相位的。距离越近,相位差越小,补偿能量传递越多。这就要求噪声源距离电容尽可能的近,要远小于**。实际应用中,这一距离最好控制在 ,这是经验数据。因此一个电容确定去耦半径的过程为:
- 计算电容的自谐振频率:,或直接从手册中找到。
- 计算信号在PCB上的传输速度:,是传播介质的相对介电常数。
- 计算对应频率信号的波长:。
- 按 确定该电容的去耦半径。
2.5.2 旁路电容
旁路电容,其作用是为前级(如电源产生的高频噪声等干扰)提供一条流到地平面的低阻抗路径,以避免这些干扰影响正在高速工作的器件。
2.5.3 耦合电容
选择交流耦合电容时,需要考虑数据帧的连续0和连续1比特位长度,用来确定选用电容的最小值。连续1相当于直流信号,给电容充电过程,接收端电压会逐渐下降,电路阻抗的耦合电容越大,压降越慢。
选择的参考公式如下:- 为每比特位的数据周期
- 为最大允许连续0或连续1比特位数目
- 为负载阻抗
耦合电容也不能取太大,太大会导致无法满足高速信号变换的边沿斜率要求。
由于电容的ESL和安装电感存在,电容在高频呈感性。电容太大,谐振点太靠前,会使高频信号的阻抗太大,造成信号衰减,上升沿变缓。 ↩ -
VCC、VDD、VSS等是什么意思
- VCC、VDD、VSS等是什么意思_vdd vss-CSDN博客
- https://blog.csdn.net/mahoon411/article/details/119301577
- 文章浏览阅读2.3w次,点赞22次,收藏157次。参考资料:电源符号:VCC、VDD、VEE、VSS、VBAT各表示什么意思?1. 解释VCC:C=circuit 表示电路的意思, 即接入电路的电压;VDD:D=device 表示器件的意思, 即器件内部的工作电压;VSS:S=series 表示公共连接的意思,通常指电路公共接地端电压;其中:对于数字电路来说,VCC是电路的供电电压,VDD是芯片的工作电压(通常Vcc>Vdd),VSS是接地点。例如,对于ARM单片机来说,其供电电压VCC一般为5V,一般经过稳压模块将其转换为单片机工_vdd vss
- 2024-09-28 14:55:35
参考资料:
电源符号:VCC、VDD、VEE、VSS、VBAT各表示什么意思?
1. 含义
VCC:C=circuit 表示电路的意思, 即接入电路的电压;
VDD:D=device 表示器件的意思, 即器件内部的工作电压;
VSS:S=series 表示公共连接的意思,通常指电路公共接地端电压;2. 说明
- 一般来说,VCC=模拟电源,VDD=数字电源,VSS=数字地。
- 对于数字电路来说,VCC是电路的供电电压,VDD是芯片的工作电压(通常Vcc>Vdd),VSS是接地点。例如,对于ARM单片机来说,其供电电压VCC一般为5V,一般经过稳压模块将其转换为单片机工作电压VDD = 3.3V
- 有些IC既有VDD引脚又有VCC引脚,说明这种器件自身带有电压转换功能。
- 在场效应管(或COMS器件)中,VDD为漏极,VSS为源极,VDD和VSS指的是元件引脚,而不表示供电电压。
3. 助记
1、VCC:C可以理解为三极管的集电极Collector或者电路Circuit,指电源正极。
2、VDD:D可以理解为MOS管的漏极Drain或者设备Device,指电源正极。
3、VEE:E可以理解为三极管的发射极Emitter,指电源负极。
4、VSS:S可以理解为MOS管的源极Source,指电源负极。
5、VBB:B可以理解为三极管的基极B,一般指电源正极。
6、AVCC,A是Analog的意思,模拟VCC,一般模拟器件会有。
7、AVDD,A是Analog的意思,模拟VDD,一般模拟器件会有。
8、DVCC,D是Digital的意思,数字VCC,一般在数字电路中。
9、DVDD,D是Digital的意思,数字VDD,一般在数字电路中。
10、AGND,模拟GND,对应AVCC或者AVDD的负极。
11、DGND,数字GND,对应DVCC或者DVDD的负极。
12、PGND,P是Power的意思,功率GND,比如DC-DC中就会有功率地和信号地区分。
13、Vpp,有的好像叫Vpk,电压峰峰值,对于正弦信号,就是波峰电压减去波谷电压,最大值减去最小值。
14、Vrms,rms是root mean squre的缩写,均方根的意思,Vrms一般指交流信号的有效值。
15、VBAT,BAT是电池BATTERY的简写,一般指电池电压。
16、VSYS,SYS是SYSTEM的简写,一般指平台方案(如MTK)的系统供电。
17、VCORE,CORE指核心的意思,一般指CPU、GPU等芯片的核电压。
18、VREF,REF是reference的意思,参考电压,比如ADC内部的参考电压等。
19、PVDD,P是Power的意思,功率VDD。
20、CVDD,这个C也是CORE,核电源VDD的意思。
21、IOVDD,IO就是GPIO,指给GPIO供电的VDD,CAMERA里面会用到,I2C通信的上拉电源。
22、DOVDD,CAMERA里面用到的,由外部供给CAMERA,一般也是模拟电源。
23、AFVDD,Auto Focus VDD,意为自动对焦VDD电源,CAMERA里面会用到,给马达供电用的。
24、VDDQ,DDR里面用到的,DDR里面有一个DQ信号,可以理解为给这些数据信号供电的。
25、VPP,这个VPP是全大写的,DDR4里面用到的,DD3里面是没有的,称为激活电压,字位线的开启电压。
26、VTT,一般VTT=1/2VDDQ,也是在DDR里面用到的,给一些控制信号提供电源的。
27、VCCQ,在nand flash里面会用到,比如手机常用的eMMC、UFS等存储器,一般给IO供电。
↩
-
三极管
三极管:电流控制型元件,因为使其导通的原理,是在BE上加电压,使其有电流经过。
三极管用的很少了,因为他的导通需要持续的电流,一块电路上有巨多的三极管,功耗还是很大的。优势就是便宜,可以用在控制led灯、电平转换电路;耐高压大电流(这点我们几乎用不到)b基极、c集电极、e发射极
B集有电流时,CBE导通;B集没有电流时,CDB截止。
三极管可以看作两个二极管的P区合为一体。但是厚度和掺杂的其他原子浓度和二极管不同。
发射集E掺杂浓度很高,基集B掺杂浓度很低且很薄。直接在两个N区接电源,是不会导通的。
为了让三极管导通,则需要在EB之间再加一个电源,使得E区的自由电子向B移动。但是B区很薄且浓度低,其余的自由电子就通过C集流入电源,形成电流
选三极管要注意的参数:耐压值、额定电流、Vbe
三极管 NPN PNP 
Base高,吸引电子,导通
Base低,排斥电子,导通 
导通之后,BE之间有电流通过
小箭头是:微小电流的流向
简单应用:
8050三极管,当USB插入时,1处高电平,三极管导通,USB_DETECT被拉低,检测到USB插入。USB未插入时USB_DETECT电平不确定,需要设置为上拉输入,与USB插入作出区分
↩ -
状态机按键消抖
- 状态机按键消抖_状态机消抖-CSDN博客
- https://blog.csdn.net/Yin_w/article/details/129570325
- 文章浏览阅读1.9k次,点赞6次,收藏36次。最近研究了一下状态机消抖,不占用MCU资源的非阻塞消抖方式。_状态机消抖
- 2024-09-28 14:59:33
嵌入式_状态机按键消抖
状态机,FSM(Finite State Machine),也称为同步有限状态机从。指的是在同步电路系统中使用的,跟随同步时钟变化的,状态数量有限的状态机,简称有限状态机。
文章目录
前言
此代码是在STMF407平台使用标准库函数实现的,需要移植时请根据实际情况进行分析和修改。
按键抖动:按键抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。这是一个很重要的时间参数,在很多场合都要用到。按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒。键抖动会引起一次按键被误读多次。为确保单片机对按键的一次闭合仅作一次处理,必须去除键抖动。在键闭合稳定时读取键的状态,并且必须判别到按键释放到稳定状态后再去作处理。
一般软件消抖一般又是直接一个简短延时Delay函数,数据阻塞式消抖效率低下,占用MCU资源,最近研究了一下状态机消抖,不占用MCU资源的非阻塞消抖。
传统硬件消抖:利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。在按键的两端并联一个0.1uf的电容
一、状态机消抖原理
如图:
初始状态:图以黑色方框表示,此时按键没有按下,MCU会检测到有一个初始电平(可以是高电平也可以是低电平),此时状态没有边沿信号触发,触次数为0,当有手按这个按键时候会产生毛刺,当检测到首次与初始电平不一样的电平信号时,就会变为初始抖动状态。初始抖动状态:图以右上角灰色方框表示,接上文,按键状态变为初始抖动状态,会持续记录与初始电平不一样的电平信号触发次数,当持续N次时表示已经是持续的反转信号了说明按键确实按下了,此时就会直接跳转到反转状态,表示按键确实被按下。如果某一次检测到的信号还是和初始电平一样,那么就会重新置为初始状态,重新记录触发次数,直至持续N次的反转信号才会跳转到反转状态。
反转状态:图以白色方框表示,接上文,此时按键确实是按下状态,MCU会检测到一个反转电平(与初始状态电平相反),此状态没有边沿信号触发时触次数为0,当有手重新按这个按键或松开按键时候会产生毛刺,当检测到首次与反转电平不一样的电平信号时,就会变为反转抖动状态。
反转抖动状态:图以左下角灰色方框表示,接上文,按键状态变为反转抖动状态,会持续记录与反转电平不一样的电平信号触发次数,当持续N次时表示已经是持续的另一种信号了说明按键确实重新按下或松开,此时就会直接跳转到初始状态,表示按键确实被重新按下或松开。如果某一次检测到的信号还是和反转电平一样,那么就会重新置为反转状态,重新记录触发次数,直至持续N次的反转信号才会跳转到初始状态。
二、实现步骤
提示:本代码基于STM32F407标准库写的,主义修改及兼容性1、一共有四个按键,定义一个按键表:
typedef enum // 按键表, { KEY0 = 0, KEY1, KEY2, KEY3, KEY_NUM, // 必须要有的记录按钮数量,必须在最后 }KEY_LIST;2、再定义一个按键属性结构体
代码如下(示例):
typedef struct { uint8_t KeysNum; //序号 GPIO_TypeDef* GPIOX; //端口 uint16_t GPIO_PinsNum; //引脚号 uint16 KEY_TIMECOUNT; //时间计数器 GPIO_Pulltype GPIO_Pull; //初始状态(未按下时候电平 0/1) KEY_STATUS key_NowStatus; //按键当前状态 uint8_t Key_Event; //按键事件 }KEY_COMPONENTS;3、再定义一个按键状态
代码如下(示例):
#define ENUM_ITEM(ITEM) ITEM, //逗号不可省略 #define ENUM_STRING(ITEM) #ITEM, #define KEY_STATUS_ENUM(STATUS) \ STATUS(KS_RELEASE) /*稳定松开状态*/ \ STATUS(KS_PRESS_SHAKE) /*按下抖动状态*/ \ STATUS(KS_PRESS) /*稳定按下状态*/ \ STATUS(KS_RELEASE_SHAKE) /*松开抖动状态*/ \ STATUS(KS_NUM) /*状态总数(无效状态)*/ \4、接口函数
extern void ScanKey(void); //持续扫描函数 extern KEY_STATUS key_status_check(uint Key_index); //状态检测函数 extern uint8_t Key_GetPinsVolt(uint8_t Pin_index); //获取GPIO电平函数5、按键结构体数据定义以及状态检测切换函数实现
KEY_COMPONENTS Key_TestGrop[KEYSNUMBERS] = { { 0,GPIOE,GPIO_Pin_4,0,PuLL_UP,KS_RELEASE,0}, //key0 { 1,GPIOE,GPIO_Pin_3,0,PuLL_UP,KS_RELEASE,0}, //key1 { 2,GPIOE,GPIO_Pin_2,0,PuLL_UP,KS_RELEASE,0}, //key2 { 3,GPIOA,GPIO_Pin_0,0,PuLL_DOWN,KS_RELEASE,0} //WE_UP }; /*分别是:编号 端口 引脚号 触发次数计数器 初始电平 初始状态 触发事件标记位*/ /************************************************************************************ *@fuction :key_status_check *@brief :状态机去抖动核心代码 *@param :-- *@return :按键状态 *@author :_Awen *@date :2022-12-10 ************************************************************************************/ KEY_STATUS key_status_check(uint Key_index) { switch(Key_TestGrop[Key_index].key_NowStatus) { case KS_RELEASE: //按键释放状态 { Key_TestGrop[Key_index].KEY_TIMECOUNT = 0; /*判断是否有触发,有触发则进入抖动状态*/ if (GET_VT(Key_index) != Key_TestGrop[Key_index].GPIO_Pull) { Key_TestGrop[Key_index].key_NowStatus = KS_PRESS_SHAKE; //变为抖动状态 Key_TestGrop[Key_index].KEY_TIMECOUNT = 0; //初始触发次数 } } break; case KS_PRESS_SHAKE://按键为触发抖动状态 { /*累计触发次数+1*/ Key_TestGrop[Key_index].KEY_TIMECOUNT++; /*判断是否有触发,无持续触发则保持正常释放状态*/ if (GET_VT(Key_index) == Key_TestGrop[Key_index].GPIO_Pull) { Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE; } /*检测到持续触发,触发次数大于最大次数,则变为按下状态*/ else if (Key_TestGrop[Key_index].KEY_TIMECOUNT > MAXDEBOUNCING_DELAY) { Key_TestGrop[Key_index].key_NowStatus = KS_PRESS; } } break; case KS_PRESS: //触发状态 { /*判断有无,无持续触发则进入释放抖动状态*/ if (GET_VT(Key_index) == Key_TestGrop[Key_index].GPIO_Pull) { Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE_SHAKE; Key_TestGrop[Key_index].KEY_TIMECOUNT = 0; } } break; case KS_RELEASE_SHAKE://释放抖动状态 { /*累计触发次数+1*/ Key_TestGrop[Key_index].KEY_TIMECOUNT++; /*判断是否有释放,无持续持续释放则保持触发状态*/ if (GET_VT(Key_index) != Key_TestGrop[Key_index].GPIO_Pull) { Key_TestGrop[Key_index].key_NowStatus = KS_PRESS; } /*检测到持续释放信号,释放次数大于最大次数,则变为释放状态*/ else if (Key_TestGrop[Key_index].KEY_TIMECOUNT > MAXDEBOUNCING_DELAY) { Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE; } } break; default:break; } /*当前状态与正常释放状态不一致时,返回触发*/ if(Key_TestGrop[Key_index].key_NowStatus != KS_RELEASE) //按键按下 { return KS_PRESS; } else { return KS_RELEASE; } }三、完整代码
1、.h文件
#ifndef __KEY_H #define __KEY_H #include "Config.h" #define KEYSNUMBERS (4U) //按键数量 #define MAXDEBOUNCING_DELAY (4U) //消抖计数 #define GET_VT(i) Key_GetPinsVolt(i) #define ENUM_ITEM(ITEM) ITEM, //逗号不可省略 #define ENUM_STRING(ITEM) #ITEM, #define KEY_STATUS_ENUM(STATUS) \ STATUS(KS_RELEASE) /*稳定松开状态*/ \ STATUS(KS_PRESS_SHAKE) /*按下抖动状态*/ \ STATUS(KS_PRESS) /*稳定按下状态*/ \ STATUS(KS_RELEASE_SHAKE) /*松开抖动状态*/ \ STATUS(KS_NUM) /*状态总数(无效状态)*/ \ typedef enum { KEY_STATUS_ENUM(ENUM_ITEM) }KEY_STATUS; typedef enum // 按键表, { KEY0 = 0, KEY1, KEY2, KEY3, KEY_NUM, // 必须要有的记录按钮数量,必须在最后 }KEY_LIST; typedef enum //未按按键状态下,按键高低电平状态 { PuLL_DOWN = 0, PuLL_UP = !PuLL_DOWN }GPIO_Pulltype; typedef struct { uint8_t KeysNum; //序号 GPIO_TypeDef* GPIOX; //端口 uint16_t GPIO_PinsNum; //引脚号 uint16 KEY_TIMECOUNT; //时间计数器 GPIO_Pulltype GPIO_Pull; //初始状态(未按下时候电平 0/1) KEY_STATUS key_NowStatus; //按键当前状态 uint8_t Key_Event; //按键事件 }KEY_COMPONENTS; extern KEY_COMPONENTS Key_TestGrop[]; extern void ScanKey(void); extern KEY_STATUS key_status_check(uint Key_index); extern uint8_t Key_GetPinsVolt(uint8_t Pin_index); #endif2、.c文件
#include "KEY_Dev.h" uint8_t KK; KEY_COMPONENTS Key_TestGrop[KEYSNUMBERS] = { { 0,GPIOE,GPIO_Pin_4,0,PuLL_UP,KS_RELEASE,0}, //key0 { 1,GPIOE,GPIO_Pin_3,0,PuLL_UP,KS_RELEASE,0}, //key1 { 2,GPIOE,GPIO_Pin_2,0,PuLL_UP,KS_RELEASE,0}, //key2 { 3,GPIOA,GPIO_Pin_0,0,PuLL_DOWN,KS_RELEASE,0} //WE_UP }; /************************************************************************************ *@fuction :key_status_check *@brief :状态机去抖动核心代码 *@param :-- *@return :按键状态 *@author :_Awen *@date :2022-12-10 ************************************************************************************/ KEY_STATUS key_status_check(uint Key_index) { switch(Key_TestGrop[Key_index].key_NowStatus) { case KS_RELEASE: //按键释放状态 { Key_TestGrop[Key_index].KEY_TIMECOUNT = 0; /*判断是否有触发,有触发则进入抖动状态*/ if (GET_VT(Key_index) != Key_TestGrop[Key_index].GPIO_Pull) { Key_TestGrop[Key_index].key_NowStatus = KS_PRESS_SHAKE; //变为抖动状态 Key_TestGrop[Key_index].KEY_TIMECOUNT = 0; //初始触发次数 } } break; case KS_PRESS_SHAKE://按键为触发抖动状态 { /*累计触发次数+1*/ Key_TestGrop[Key_index].KEY_TIMECOUNT++; /*判断是否有触发,无持续触发则保持正常释放状态*/ if (GET_VT(Key_index) == Key_TestGrop[Key_index].GPIO_Pull) { Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE; } /*检测到持续触发,触发次数大于最大次数,则变为按下状态*/ else if (Key_TestGrop[Key_index].KEY_TIMECOUNT > MAXDEBOUNCING_DELAY) { Key_TestGrop[Key_index].key_NowStatus = KS_PRESS; } } break; case KS_PRESS: //触发状态 { /*判断有无,无持续触发则进入释放抖动状态*/ if (GET_VT(Key_index) == Key_TestGrop[Key_index].GPIO_Pull) { Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE_SHAKE; Key_TestGrop[Key_index].KEY_TIMECOUNT = 0; } } break; case KS_RELEASE_SHAKE://释放抖动状态 { /*累计触发次数+1*/ Key_TestGrop[Key_index].KEY_TIMECOUNT++; /*判断是否有释放,无持续持续释放则保持触发状态*/ if (GET_VT(Key_index) != Key_TestGrop[Key_index].GPIO_Pull) { Key_TestGrop[Key_index].key_NowStatus = KS_PRESS; } /*检测到持续释放信号,释放次数大于最大次数,则变为释放状态*/ else if (Key_TestGrop[Key_index].KEY_TIMECOUNT > MAXDEBOUNCING_DELAY) { Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE; } } break; default:break; } /*当前状态与正常释放状态不一致时,返回触发*/ if(Key_TestGrop[Key_index].key_NowStatus != KS_RELEASE) //按键按下 { return KS_PRESS; } else { return KS_RELEASE; } } uint8_t Key_GetPinsVolt(uint8_t Pin_index) { return GPIO_ReadInputDataBit(Key_TestGrop[Pin_index].GPIOX,Key_TestGrop[Pin_index].GPIO_PinsNum); } /************************************************************************************ *@fuction :ScanKey *@brief : *@param :-- *@return :按键状态 *@author :_Awen *@date :2022-12-10 ************************************************************************************/ void ScanKey(void) { uint8_t i = 0; for(i = 0;i < KEY_NUM;i++) { if(key_status_check(i) == KS_PRESS) { KK = i; } } }四、使用方法
1、根据实际需求配置按键表和Key_TestGrop结构体。
2、在while循环或子任务中调用void ScanKey(void)函数.
3、在需要判断按键是否按下或释放的位置调用KEY_STATUS key_status_check(uint Key_index),判断其返回值即可.
总结
1、这里只对反转两种电平信号作判断,扩展一下可以做按下按键、长按按键、短按按键、双击或三击按键做判断。
2、状态机应用非常实用,尤其是Switch case语句常用来作为多状态切换,不同状态使用不同功能的一种常用框架,必须掌握。
如有错误,欢迎指正,原创不易,转载留名! ↩ -
使用串口下载程序_stm32串口下载
- STM32 使用串口下载程序_stm32串口下载-CSDN博客
- https://blog.csdn.net/m0_73640344/article/details/133131900
- 文章浏览阅读2.7k次,点赞6次,收藏11次。本文介绍了STM32使用串口下载程序的过程,包括BootLoader的作用、启动配置以及如何解决每次串口下载都需要切换跳线帽的问题。通过BootLoader,STM32可以在接收串口数据后更新程序,即使在更新期间主程序处于瘫痪状态。
- 2024-09-28 15:03:10
STM32 使用串口下载程序
STM32 使用串口下载程序
1.串口下载的原理
-
在ROM区的0x0800 0000位置,存储的就是编译后的程序代码,你把什么程序写入到这个位置,STM32就执行什么样的程序。如果想使用串口下载程序的话,我们只需要把数据通过串口发送给STM32,STM32接收数据,然后刷新到0x0800 0000这一块位置就行了。但是接收并转存数据,这个过程本身也是程序,如果利用程序进行自我更新,这是一个问题。
就像是一个机器人,给自己换电池一样,换电池,需要先拆掉旧电池,再装上新电池,但是一旦把旧电池拆掉,机器人本身就无法工作了,这样之后装上新电池的工作就没法完成了,所以为了能让机器人换电池,我们还需要再额外做一个小机器人,需要换电池的时候,就启动这个小机器人,小机器人完成整个换电池的工作之后,再返回大机器人运行。
同理,STM32通过串口进行程序的自我更新,就需要这样一个小机器人,这个小机器人就是BootLoader,BootLoader是ST公司写好的一段程序代码,这段程序的存储位置,就是ROM区的最后,0x1FFF F000,这段区域叫做系统存储器,存储的是BootLoader程序,或者叫自举程序。用途是程序自我更新,串口下载。在更新过程中,BootLoader接收USART1数据,刷新到程序存储器Flash,这是主程序就处于瘫痪状态,更新好之后再启动主程序,执行新程序,这就是串口下载的流程。
1.1启动配置
- 当Boot0 为0时,就是从主闪存,也就是0x0800 0000的位置开始运行
- 当Boot0为1,Boot1为0时,就是从系统存储器,也就是0x1FFF F000开始运行
- 当Boot0为1,Boot1为1时,就是从SRAM,也就是0x2000 0000开始运行
由于系统复位后,在SYSCLK的第四个上升沿,BOOT引脚的值将被锁存,所以说,每次切换Boot引脚之后,都要按一下复位。
2.每次串口下载都要切换跳线帽,如何解决
- 1.STM32一键下载电路
- 或者在FlyMcu中选择编程后执行,并且取消选择编程到FLASH时写选项字节
-
由于其在软件上,人工加入了一条跳转指令(成功从0x0800 0000开始运行),这样就能执行主程序了,但是这样只是一次性的,复位之后,执行的程序仍然是BootLoader ↩
-
程序的烧录方式 与 ISP一键下载
- STM32程序的烧录方式 与 ISP一键下载_dtr的低电平复位,rts高电平进bootloader-CSDN博客
- https://blog.csdn.net/tyler880/article/details/90311773
- 文章浏览阅读2.6w次,点赞30次,收藏194次。一、启动模式(Boot modes)阅读:STM32中文参考手册_V10.pdf 查看启动配置(Boot modes)。在STM32F10xxx里,可以通过BOOT[1:0]引脚选择三种不同启动模式。STM32三种启动模式对应的存储介质均是芯片内置的,它们是:1)用户闪存 = 芯片内置的Flash。2)SRAM = 芯片内置的RAM区,就是内存啦。3)系统存储器 = 芯片内部一块…_dtr的低电平复位,rts高电平进bootloader
- 2024-09-28 15:03:49
一、启动模式(Boot modes)
阅读:STM32中文参考手册_V10.pdf 查看启动配置(Boot modes)。
在STM32F10xxx里,可以通过BOOT[1:0]引脚选择三种不同启动模式。
STM32三种启动模式对应的存储介质均是芯片内置的,它们是:1)用户闪存=芯片内置的Flash。2)系统存储器=芯片内部一块特定的区域,芯片出厂时在这个区域预置了一段Bootloader,就是通常说的ISP程序。这个区域的内容在芯片出厂后没有人能够修改或擦除,即它是一个ROM区。3)SRAM=芯片内置的RAM区,就是内存啦。
在每个STM32的芯片上都有两个管脚BOOT0和BOOT1,这两个管脚在芯片复位时的电平状态决定了芯片复位后从哪个区域开始执行程序,见下表:BOOT1=x BOOT0=0 从用户闪存启动,这是正常的工作模式。BOOT1=0 BOOT0=1 从系统存储器启动,这种模式启动的程序功能由厂家设置。BOOT1=1 BOOT0=1 从内置SRAM启动,这种模式可以用于调试。
在系统复位后, SYSCLK的第4个上升沿, BOOT引脚的值将被锁存。用户可以通过设置BOOT1和BOOT0引脚的状态,来选择在复位后的启动模式。
在从待机模式退出时, BOOT引脚的值将被被重新锁存;因此,在待机模式下BOOT引脚应保持为需要的启动配置。在启动延迟之后, CPU从地址0x0000 0000获取堆栈顶的地址,并从启动存储器的0x0000 0004指示的地址开始执行代码。
因为固定的存储器映像,代码区始终从地址0x0000 0000开始(通过ICode和DCode总线访问),而数据区(SRAM)始终从地址0x2000 0000开始(通过系统总线访问)。 Cortex-M3的CPU始终从ICode总线获取复位向量,即启动仅适合于从代码区开始(典型地从Flash启动)。 STM32F10xxx微控制器实现了一个特殊的机制,系统可以不仅仅从Flash存储器或系统存储器启动,还可以从内置SRAM启动。
根据选定的启动模式,主闪存存储器、系统存储器或SRAM可以按照以下方式访问:
● 从主闪存存储器启动: 主闪存存储器被映射到启动空间(0x0000 0000),但仍然能够在它原有的地址(0x0800 0000)访问它,即闪存存储器的内容可以在两个地址区域访问, 0x0000 0000 或 0x0800 0000。
● 从系统存储器启动: 系统存储器被映射到启动空间(0x0000 0000),但仍然能够在它原有的地址(互联型产品原有地址为0x1FFF B000,其它产品原有地址为0x1FFF F000)访问它。(可用于串口下载)● 从内置SRAM启动: 只能在0x2000 0000开始的地址区访问SRAM。
注意: 当从内置SRAM启动,在应用程序的初始化代码中,必须使用NVIC的异常表和偏移寄存器,从新映射向量表之SRAM中。
1, ST-LINK烧写
1.1 ST-LINK烧写的SWD模式
ST-LINK烧写的SWD模式 是ST-LINK烧写的一种方式,只需要4根接线。 分别为VCC,GND,SWCLK, SWDIO;
如果用SWD模式下载的话,只需要接:J-LINK的第1脚(VDD)、第7脚(TMS/SWDIO对应stm32的PA13)、第9脚(TCK/SWCLK对应stm32的PA14)、第4.6.8.10.12.14.16.18.20中的任意一个脚(GND地脚)、需要说明的是第15脚(RESET对应stm32的NRST)可接可不接,大家根据实际自己决定(保险起见还是建议接上,不接时可以在MDK仿真器的设置里面不使用硬件复位,而是用system reset或者vect reset)!
用SWD下载参考电路图:
1.2 ST-LINK烧写的JTAG模式
在JTAG模式下的程序烧写过程中需要进行单独对板子进行供电
STM32F10XXX JTAG的引脚:
STM32F10XXX 芯片内部的JTAG引脚已带有内部上拉和下拉:
JTAG下载参考电路图:
2 ,USB转串口连接线烧写(又称ISP烧写,且使用的串口必须是串口1)
2.1 方法一(该方法用于手动设置下载)
Step1:将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader;
Step2:最后在BootLoader的帮助下,通过串口下载程序到Flash中;
Step3:程序下载完成后,必须要将BOOT0设置为GND,手动复位,这样,STM32才可以从Flash中启动。2.2 方法二(该方法用于软件一键下载)
我们想用串口下载代码,就要配置BOOT0为1,BOOT1为0,但是如果想让STM32一复位就运行代码,就要配置BOOT0为0,BOOT1配置为什么都可以,为了解决这个问题,我们可以设计一个电路,通过串口转USB芯片CH340G的DTR#和RTS#引脚的信号来控制一键下载电路,从而间接控制STM32的RESET和BOOT0引脚的信号,来达到通过串口一键下载和运行的效果。
一键下载电路参考:
串口下载软件选用的是FlyMcu或MCUISP,通过串口的DTR和RTS信号来自动配置BOOT0和RESET信号,不需要用户手动切换它们的状态,直接串口软件自动控制,可以方便的下载代码。
我们需要注意一点:CH340G上电后DTR**#和RTS#都为高电平,在用MCUISP烧写软件时,我们在软件下方选择“DTR的低电平复位,RTS高电平进BootLoader”,CH340G IC在实际操作时引脚的变化为“DTR拉高,RTS#拉低”,即软件设置和实际情况是取非的,相反的。**
首先,FlyMcu软件控制DTR输出低电平,则DTR#引脚输出高, 然后RTS置高,则RTS#引脚输出低,这样Q3导通了,BOOT0被拉高,即实现设置BOOT0为1,同时Q2也会导通,STM32的复位脚被拉低,实现复位。
然后,延时100ms后,FlyMcu软件控制DTR为高电平,则DTR#引脚输出低电平,RTS维持高电平,则RTS#引脚继续为低电平,此时STM32的复位引脚,由于Q2不再导通,变为高电平,STM32结束复位,但是BOOT0还是维持为1,从而进入ISP模式,接着mcuisp就可以开始连接STM32,下载代码了,从而实现一键下载。
DTR#和RTS#信号的时序图如下图所示:

程序下载完毕后,如果设置了编程后执行,STM32会再次被复位,此时DTR#引脚为高,RTS#引脚为低,STM32复位后,DTR#引脚设置为低,RTS#引脚设置为高,那么Q2和Q3都不导通,此时,STM32重新开始启动后,检测到BOOT0为0,程序开始正常运行,一键下载至此就完成了。参考:https://blog.csdn.net/weixin_42295502/article/details/80916124
