STM32杂项
记录单片机在工作中遇到的问题和特殊情况。
1.启动过程
M3/M4/M7内核复位后,做的第一件事:
1.从地址0x0000 0000处取出堆栈指针MSP的初始值,该值就是栈顶地址。
2.从地址0x0000 0004处取出程序计数器PC的初始值,该值是复位向量。
在系统复位后,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存。
2.中断
NVIC-管理中断;
EXTI-外部中断控制器;
中断:打断CPU执行正常的程序,转而处理紧急程序,然后返回原暂停的程序继续运行。
中断向量表:
定义一块固定的内存,以4字节对齐,存放各个中断服务函数程序的首地址。
中断向量表定义在启动文件,当发生中断,CPU会自动执行对应的中断服务函数。
NVIC相关寄存器:
抢占优先级(pre):高优先级可以打断正在执行的低抢占优先级中断。
响应优先级(sub):当抢占优先级相同时,响应优先级高的先执行,但不能相互打断。
抢占和响应都相同的情况下,自然优先级越高的,先执行。
自然优先级:中断向量表中的优先级。
数字越小,表示优先级越高。
分组:
优先级分组 | 分配结果 |
---|---|
0 | 0位抢占优先级,4位响应优先级 |
1 | 1位抢占优先级,3位响应优先级 |
2 | 2位抢占优先级,2位响应优先级 |
3 | 3位抢占优先级,1位响应优先级 |
4 | 4位抢占优先级,0位响应优先级 |
一个工程中,一般只设置一次中断优先级分组。多次的话,以最后一次为准。
3.GPIO
工作电压范围:2V<VDD<3.6V。
识别电压范围:
低电压:-0.3V<V<1.164V。
高电压:1.833V<V<3.6V。
输出电流:单个IO,最大25MA。
类型:电源引脚,晶振引脚,复位引脚,下载引脚,BOOT引脚,GPIO引脚。
4.Systick
systick:系统滴答定时器,包含在M3/M4/M7内核里面,核心是一个24位的递减计数器。
72mhz经过分频器(CLKSOURCE),每来一个时钟,VAL自动减1,当VAL减到0时,COUNTFLAG变成1(可以判断这位是否为1,来判断是否到达延时时间,退出等待),并且LOAD的值自动赋给VAL重新开始计时。
其中VAL和LOAD的值为2的24次方(0~16777215)。
以下是不同的几种延时函数的写法:
/*
第一种写法:不需要进行延时初始化,每执行一遍Delay_us()
相当于配置一遍滴答定时器,等待COUNTFLAG变成1。当不在调
用延时函数时停止滴答定时器。
*/
void Delay_us(uint32_t xus)
{
SysTick->LOAD = 72 * xus; //设置定时器重装值 2的24次方 16777216
SysTick->VAL = 0x00; //清空当前计数值
SysTick->CTRL = 0x00000005; //设置时钟源为HCLK,启动定时器
while(!(SysTick->CTRL & 0x00010000)); //等待计数到0
SysTick->CTRL = 0x00000004; //关闭定时器
}
void Delay_ms(uint32_t xms)
{
while(xms--)
{
Delay_us(1000);
}
}
void Delay_s(uint32_t xs)
{
while(xs--)
{
Delay_ms(1000);
}
}
/*第二种写法:需要进行延时的初始化,在初始化的时候配置滴答定时器
过多长时间进行一次中断(通常为1ms),此后不在配置。定义一个系统时间,
在定时器中断里进行加一。需要进行延时处理的时候,在延时函数里定义一
个新的变量,并将系统时间和需要延时的时间相加赋给这个变量,然后等待
系统时间等于这个变量。
*/
vu32 System_Time = 0;//系统时间
void SysTick_Handler(void)
{
System_Time++;//1ms自加一次
}
void delay_ms(uint32_t time)
{
uint32_t tick = System_Time + time;
while(System_Time < tick) //等待系统时间等于tick
{}
}
//系统定时器的初始化
//想要1ms来一次中断
//参数:重装载值
void SysTick_Init(uint32_t reload)
{
if(SysTick_Config(reload))
{
while(1);
}
}
void delay_us(uint32_t time)
{
while(time--)
delay_1us();
}
while(1)
{
//延时初始化 1ms
SysTick_Init(72000);
}
/*
由于这个延时时间是以ms为单位的,需要us延时时可以加入汇编中
的——NOP()命令进行空等待。
*/
#define delay_1us() {\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();\
}
/*延迟初始化函数,实际是调用了SysTick_Config()函数,
这个函数为标准库中函数库 */
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
//判断自动重装值是否超过界限。
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1);
//设置自动重装值
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;
//设置滴答定时器中断
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
//将VAL清零
SysTick->VAL = 0;
//配置CTRL 1分频,打开中断,开滴答定时器
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
return (0);
}
5.串口printf
printf函数输出流程:
我们通过需要输出的硬件来重新定义fputc()函数.
/*
重定义fputc函数,使printf()函数在打印的过程中,
也将数据通过串口进行输出
*/
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}
// 其他串口使用printf()
/*
方法一: 定义一个数组,使用sprintf()函数进行中转一下
再用该串口发送字符串的函数进行发送。
*/
char string[100];
sprintf(string,"num=%d\r\n",555);
serial_sendstring(string);
/*
方法二:封装sprintf()
*/
void Serial_Printf(char *format, ...)
{
char String[100]; //定义字符数组
va_list arg; //定义可变参数列表数据类型的变量arg
va_start(arg, format); //从format开始,接收参数列表到arg变量
vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中
va_end(arg); //结束变量arg
Serial_SendString(String); //其他串口发送字符数组(字符串)
}
6.独立看门狗
IWDG:独立看门狗
本质:可以产生系统复位信号的计数器。
特点:时钟为低速时钟LSI(40kHZ)由独立的RC振荡器提供(可在停止和待机模式下运行)
看门狗被激活后,当递减计数器计数到0x000时,产生系统复位。
喂狗:在计数器计数到0之前要进行喂狗,防止系统一直被复位。
/*IWDG初始化*/
//独立看门狗的时钟为40khz 低速时钟LSI驱动
void IWDG_init()
{
//独立看门狗写使能
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
//设置预分频为16
IWDG_SetPrescaler(IWDG_Prescaler_16);
/设置重装值为2499,独立看门狗的超时时间为1000ms 范围0~4095
IWDG_SetReload(2499);
//重装计数器,喂狗
IWDG_ReloadCounter();
//独立看门狗使能
IWDG_Enable();
}
7.特殊引脚作为普通IO使用
PC14、PC15默认为连接32.768K低速外部晶振,PB3、PB4、PA15是JTAG调试脚,PB4、PA15为SWJ调试,PB3异步跟踪。
PWR->CR |= 1<<8; //取消备份区写保护
RCC->BDCR &= 0xFFFFFFFE; //关闭外部低速振荡器,PC14,PC15成为普通IO
BKP->CR &= 0xFFFFFFFE; //侵入检测TAMPER引脚(PC13)作为通用IO口使用
PWR->CR &= 0xFFFFFEFF; //备份区写保护</span>
///
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE); //切换到SWJ调试,释放PA15,PB4
DBGMCU->CR &= 0xFFFFFFDF; //禁止异步跟踪,释放PB
8.指针函数和函数指针
指针函数:本质是一个函数,其返回值是指针。
函数指针:本质是一个指针,其指向一个函数。
指针函数:int *fun(int x, int y); //int * 是一个整体修饰fun函数的返回值
函数指针:int (*fun)(int x, int y); //(*int)是一个整体。相当于把要指向的函数的地址赋给fun函数。两者都表示同一个函数
//指针函数
int *max(int x,int y)
{
if(x>y)
return x;
else
return y;
}
//函数指针
int (*fun)(int x,int y);
int add(int x,int y)
{
return x+y;
}
//main
int main()
{
int *val=NULL;
val = max(10,20); //把函数返回的指针赋给val
printf("Max: %d\n", *maxPtr); // 输出最大值 20
printf("Max: %p\n", maxPtr); //地址 000000f054b0f9f0
/****************************/
int val2=0;
fun=add; //把函数add的地址给fun函数。fun函数指向add函数。
//fun = &add; //与上一行表示结果一样。
val2=add(10,20);
printf("Result of add: %d\n", add); //30
val2=fun(10,20);
printf("Result of add: %d\n", add); //30
}
9.预处理-条件编译
#if,#elif,#ifndef,#ifdef,#else,#endif
1.#ifndef 和 #ifdef
这两的区别是:
#ifndef:如果后面的宏没有被#define给定义,则执行后面的代码
#ifdef: 如果后面的宏被#define给定义,则执行后面的代码
2.#if 和 #ifdef
这两的区别是:
#if: 判断后面的条件是否成立,成立则执行。
#ifdef: 判断后面的宏是否被#define给定义,定义了则执行后面的代码
3.例子
#define debug
#define if_val 10
#define if_val2 0
#ifndef debug2
printf("#ifndef被执行\n");
#else
printf("#ifndef没有被执行\n");
#endif
#ifndef debug
printf("#ifdef被执行\n");
#else
printf("#ifdef没有被执行\n");
#endif
#if if_val >15
printf("#if被执行,if_val>15 \n");
#elif if_val >5
printf("#if被执行,if_val>5 \n");
#endif
#if if_val2
printf("if_val2为0时,#if被执行 \n");
#else
printf("if_val2为0时,#if没有被执行\n");
#endif
10. 串口接收不定长数据
1. 接收中断加空闲中断。
同时开启串口的接收中断和空闲中断,当有数据连续过来时,进入到接收中断里将数据存放起来。当这帧数据发送完成后,到再次发送时,中间的时间间隔大于空闲超时时间时,进入到空闲中断里,在这里进行将接收完成的标志位给置一,同时归零接收数据个数的标志位。
具体步骤
1.在初始化串口函数时开启空闲中断。
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
2.在串口中断函数里加入判断空闲中断标志位。
void USART1_IRQHandler(void)
{
u8 dummy_data;
//接收中断
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
rxBuffer[rxIndex++] = USART_ReceiveData(USART1);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
//空闲中断
if (USART_GetITStatus(USART1, USART_IT_IDLE) == SET)
{
Serial_RxFlag = 1;
// rxIndex=0; //接收数据个数标志位,可以在这给归零,也可以在数据处理的时候给归零
USART_ClearFlag(USART1,USART_FLAG_IDLE);
dummy_data= USART1->SR; //清除空闲中断标志位。读取即可
dummy_data = USART1->DR;
}
}
3.在主函数里进行数据处理
if (Serial_GetRxFlag() == 1)
{
Serial_SendArray(rxBuffer,rxIndex); //数据回传
rxIndex=0; //接收数据个数标志位归零
}
2. 接收中断加定时器中断。
当单片机的功能不是很好时,可以没有空闲中断,这时可以通过开一个定时器来模拟空闲中断。原理和空闲中断一样。都是判断接收完一个字节后,总线空闲的时间,如果超过这个时间,则断定这一帧数据发送完毕。
具体步骤:
1.初始化串口接收中断配置函数。
2.初始化一个时间为100ms(超时时间)的定时器,默认不开启。(具体时间,按自己的实际情况来)
3.在接收中断中打开定时器,并将接收到的数据放到一个数组里,同时清零该定时器的计数值CNT。
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
TIM_Cmd(TIM2,ENABLE); //开定时器
rxBuffer[rxIndex++] = USART_ReceiveData(USART1); //接收数据
TIM_SetCounter(TIM2,0); //清理计数值
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
4.在定时器中断里,将接收完成的标志位值一,和清除计数值以及关定时器。
(当进入到定时器中断时表示空闲时间已经超过超时时间, 数据已经发送完成)
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Serial_RxFlag = 1; //接收完成的标志位值一
// rxIndex=0; //接收数据个数标志位,可以在这给归零,也可以在数据处理的时候给归零
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
TIM_SetCounter(TIM2,0); //清理计数值
TIM_Cmd(TIM2,DISABLE); //关定时器
5.在主函数里进行数据处理
if (Serial_GetRxFlag() == 1)
{
Serial_SendArray(rxBuffer,rxIndex); //数据回传
rxIndex=0; //接收数据个数标志位归零
}
3. 接收中断加空闲中断加DMA。
将串口配置成空闲中断和MDA接收。当有串口有数据发过来时,并不会进入到中断里进行接收,而是通过DMA进行转运,把接收到的数据自动的放到一个自己定义的数组里。等到一帧数据全部接收完毕时,这时会产生一个空闲中断。在空闲中断里可以通过DMA_GetCurrDataCounter();函数来判断自己接收了多少个数据,并且把接收完成的标志位给置一。
注:DMA_GetCurrDataCounter();函数是判断转运数组还剩下多少个空间。
实际接收的数据个数为:定义数据的大小-DMA_GetCurrDataCounter()
标准库的代码和详细解释,放到这篇文章里了。
…
STM32CubeMX具体的代码的配置,放到这篇文章里了。
使用STM32CubeMX配置串口各种功能
11.串口硬件流控制(未完结)
串口除了TX,RX线外,还有CK,CTS,RTS这三跟线。
串口硬件流控制是一种用于协调数据发送端和接收端之间数据传输速率的机制。
它主要是为了防止接收端数据缓冲区溢出(接收数据太快而来不及处理)或者发送端发送数据过多而接收端无法及时接收的情况。
RTS:请求发送。CTS:清除发送。
12.单一个“{}”的作用
程序里只出现了一对“{}”,而上面没有其他的关键词(如 if,while,for,switch之类)的话,这表面这对“{}”的意思是限制变量的作用域。把变量的作用域和生存周期定在括号里面,有的类似于子函数里的局部变量。
void main()
{
{
int a=0;
printf("%d",a)
}
printf("%d",a)
}
在这段程序中,编译器会进行报错。因为第一个printf可以正常输出。但第二个printf则会找不到a。
13.__weak关键字
用来修饰函数表示“弱”函数。当其他文件有相同函数名时,使用新的函数,没有时使用该函数。
void main()
{
printf("%d",num());
}
int num()
{
return 5;
}
//另一个.c文件///
__weak int num()
{
return 10;
}
14.volatile关键字
编译器在用到这个变量时必须每次都重新从变量对应的地址里读取这个变量的值,而不是使用保存在寄存器里的备份。
具体案例:
1.读写外部设备寄存器时。
2.中断的标志位。
3.多线程共享变量。(注意不能代替互斥量和队列)
/*一.并行设备的硬件寄存器。当声明指向设备寄存器的指针时一定要加vilatile,
他会告诉编译器一定要逐行的生成机器代码。
*/
#define XBYTE ((volatile unsigned char *)0x8000)//假设硬件寄存器的基地址
void set_register()
{
XBYTE[2]=0x55;
XBYTE[2]=0x66;
XBYTE[2]=0x77;
XBYTE[2]=0x88;
}
//如果未声明volatile的话,编译可能优化为最后的值0x88,。声明过之后,编译器会逐行的生成机器代码,确保硬件设备能够接收到完整的操作序列
/*二.中断服务程序中修改的变量。告诉编译器后面这个值可能会随时的被修改,
每次读取一定要从该变量对应的地址里读取,不要使用暂存在寄存器中的值
*/
volatile bool flag=false; //中断标志位
void ISR()
{
flagtrue; //中断触发时修改变量
}
/*三.多线程中共享的变量。确保每个线程读取到的变量都是最新值,而不是被优化后缓存值。*/
volatile bool stop = false;//共享变量
void *thread_func(void* arg)
{
while(!stop)
{
//执行线程操作
}
]
int main()
{
stop = true;//通知线程停止
}
//注意:这个关键字不能替换互斥锁和队列
15.单片机死机/跑飞原因
16.32位与8位单片机
我们知道在计算机中,CPU不能直接与硬盘进行数据交换,只能直接跟内存进行数据交换。而CPU是通过地址总线,数据总线,控制总线三条线与内存进行数据传输与操作。
1.假如我们想通过CPU在内存中读取一个数据3。那应该怎么操作呢?
首先,CPU需要通过地址总线,在内存中找到数字3的地址。
然后,通过控制总线判断是读还是写。
最后,通过数据总线将数据传输到CPU中。
2.地址总线宽度决定CPU的寻址能力,或者说最大访问内存容量。
数据总线宽度决定CPU单次数据传输的传送量。(也就是一次可以处理的数据的宽度).
3.我们平常说的计算机或者单片机是8位,16位,32位,或者64位是值CPU中通用寄存器一次性处理,传输,暂时存储信息的最大长度。也就是数据总线宽度。
4.指针的本质是一个整数,它储存的是内存地址。通过指针能够访问到内存中所有的地方。某计算机是32位的,他的数据总线宽度也就是32字长。一次所处理的信息也就是32位。每位可能是0或者1。也就是有2的32次方种地址。
反过来,每位为0/1,即32位0或1的组合。即指针的地址就是占4个字节。
32位中-数据宽度位32-指针占4个字节。
64位中-数据宽度位64-指针占8个字节。