STM32开发

  持续断断续续更新ing;现已更新完毕~

一、迁移工程

  keil建立工程,需要添加启动文件,需要修改魔术棒设置,需要添减文件和文件路径等等……,不难,倒是很麻烦;如果有现成的工程(往往都会有现成的工程),只需要稍微修改工程信息,借助这个基础工程会节省点偷懒点时间;故,说明下如何简单快速的修改工程;简单3个步骤很实用!!!

  1. 在工程文件目录中,将old.uvoptx和old.uvprojx名字改成new.uvoptx和new.uvprojx。并其他 .uvoptx文件统统删除。如:
  1. 双击打开new.uvproj,点击,在弹出的界面上,双击“Progect target”下面的工程名,修改成new
  1. 点击魔术棒,将Name of Executeable中的名称改为new即可,这表示生成的hex文件是new.hex,F7编译一遍,再双击new工程名字,说明形成了新的new.map文件,建立成功了
  1. 注意:最好先kill重编译,若编译后还有带不需要的文件,删除后重新编译即可。

二、常见驱动异常

2.1 驱动异常 & 复用时钟

  AFIO时钟只是在STM32F1系列里被提及。 对于32F1系列,涉及到管脚的EXTI、 REMAP、默认复用管脚(JTAG管脚)、事件输出时就需要开启AFIO时钟。

Bug现象:刚写一个 功能(驱动),出现异常;经过多次排查后,发现是IO管脚驱动初始化的问题。经测试,部分IO位无法正常响应置复位的操作。

问题来源:

  1. IO时钟使能错误;
  2. 外设时钟未使能;
  3. 该管脚为默认复用管脚;

举例:STM32默认启动时PB4、PB3、PA15三个引脚不是普通IO,而是JTAG的复用功能,分别为JNTRST、JTDI、JTDO。如果作为普通IO使用,要先开启复用时钟,再关闭JTAG复用功能,才能作为普通IO口使用。

1
2
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);	//使能复用时钟
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);//关闭jtag,使能SWD,可以用SWD模式调试

划重点:初始化中,必须使能 复用端口RCC_APB2Periph_AFIO的时钟 才能对应关闭JTAG功能(没开启复用时钟,只关闭JTAG复用的操作是无效的)。


  对于STM32F1以外的其它STM32系列,不再提AFIO,多了个SYSCFG(系统控制器)外设,其功能跟F1的AFIO 有些类似但有差异:主要管理内存空间的映射、与EXTI中断源有关的IO配置以及其它配置等事务,不同32系列间还各有细小差异。如果不打开SYSCFG时钟,有关内存空间重映射、与EXTI配置的操作就会无效。

2.2 宏定义值错误

  库函数的实现,实际上就是把寄存器抽象成一个个变量,宏定义很多替换值对其进行赋值操作;由于可能不同寄存器在同一个32位bit内,因此赋值操作过程中会进行位与操作,防止改写掉其他寄存器配置。

  因此,往往库函数的宏定义配置,其替换值往往都是32bit的;类型都一样,编译器是无法识别出你赋错值,例如你写错宏定义进去,编译器并不会报错。


举具体两个例子:

1
2
3
//错误的写法
RCC_APB1PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能 GPIOB端口 时钟
RCC_APB2PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); //使能 串口2 时钟

  很明显,上面两行代码的时钟使能是错误的,但是编译是不会报错的;因为本质上就是一个赋值操作,赋错值并不会产生报错,除非类型不同、宏定义名称查询不到。不单单是IO的驱动,不管是什么驱动,大家在配置底层驱动的时候一定要细心,配置错误不会报错警,但是会卡住你项目进度。

1
2
3
4
5
6
7
8
9
//错误的写法
GPIO_PinAFConfig(GPIOC,GPIO_Pin_10,GPIO_AF_SPI3);
GPIO_PinAFConfig(GPIOC,GPIO_Pin_11,GPIO_AF_SPI3);
GPIO_PinAFConfig(GPIOC,GPIO_Pin_12,GPIO_AF_SPI3);

//正确的写法
GPIO_PinAFConfig(GPIOC,GPIO_PinSource10,GPIO_AF_SPI3);
GPIO_PinAFConfig(GPIOC,GPIO_PinSource11,GPIO_AF_SPI3);
GPIO_PinAFConfig(GPIOC,GPIO_PinSource12,GPIO_AF_SPI3);

三、STM32管脚四种输出模式对比

  • 普通推挽输出( GPIO_Mode_Out_PP ) :
    • 使用场合:一般用在0V 和 3.3V 的场合。线路经过两个P_MOS和 N_MOS管,负责上拉和下拉电流。
    • 使用方法:直接使用
    • 输出电平:推挽输出的低电平是 0V,高电平是 3.3V。
  • 普通开漏输出( GPIO_Mode_Out_OD ):
    • 使用场合:一般用在电平不匹配的场合,如需要输出 5V 的高电平。使用方法:就需要再外部接一个上拉电阻,电源为5V,把 GPIO设置为开漏模式,当输出高组态时,由上拉电阻和电源向外输出5V 的电压。
    • 输出电平:在开漏输出模式时,如果输出为0,低电平,则使 N_MOS导通,使输出接地。若控制输出为 1(无法直接输出高电平), 则既不输出高电平也不输出低电平, 为高组态。为正常使用,必须在外部接一个上拉电阻。
    • 特性: 它具“线与”特性,即很多个开漏模式引脚连接到一起时,只有当所有引脚都输出高阻态, 才由上拉电阻提供高电平 , 此高电平的 电压 为外部 上拉电阻所接的 电源 的电压。若其中一个引脚为低电平,那线路就相当于短路 接地 ,使得整条线路都为低电平,0 V。
  • 复用推挽输出( GPIO_Mode_AF_PP ) : 用作串口的输出。
  • 复用开漏输出( GPIO_Mode_AF_OD ):用在 IIC 。

Ps:所有的开漏输出都需要接上拉电阻

四、中断优先级

  中断的概念具体就不赘述,这里以stm32F103RBT6芯片为例讲中断配置

4.1 抢占优先级和响应优先级

  STM32的中断向量具有两个属性,一个为抢占属性,另一个为响应属性,其属性编号越小,表明它的优先级别越高。
  抢占,是指打断其他中断的属性,即因为具有这个属性会出现嵌套中断(在执行中断服务函数A 的过程中被中断B打断,执行完中断服务函数B,再继续执行中断服务函数A),抢占属性由NVIC_IRQChannelPreemptionPriority 的参数配置。
  响应,应用在抢占属性相同的情况下,当两个中断向量的抢占优先级相同时,如果两个中断同时到达,则先处理响应优先级高的中断, 响应属性由NVIC_IRQChannelSubPriority 参数配置。
  如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行

4.2 NVIC 的优先级组

  STM32的NVIC 有十六个优先级。

(一)STM32分组为:组0-4

(二)分组配置在寄存器SCB->AIRCR中:

(三)解析第二点

  1. 组0就是4位都用来设置成响应优先级,2^4=16位都是响应优先级
  2. 组1分为(2^1)两个抢占优先级,在这两个抢占优先级里面还分别有(2^3)八个响应优先级,(2^1)*(2^3) =16
  3. 组2分为(2^2)四个抢占优先级,在这四个抢占优先级里面还分别有(2^2)四个响应优先级,(2^2)*(2^2) =16
  4. 组3分为(2^3)八个抢占优先级,在这八个抢占优先级里面还分别有(2^1)两个响应优先级,(2^3)*(2^1) =16
  5. 组4分为(2^4)十六个都是抢占优先级(2^4) =16

(四)配置示例

1
2
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
//也就是说可以配置成四个抢占优先级,在这四个抢占优先级中还可以配置四个响应优先级

(五)优先级顺序举例

  假定设置中断优先级组为2,然后设置如下:

  • 中断3(RTC中断)的抢占优先级为2,响应优先级为1
  • 中断6(外部中断0)的抢占优先级为3,响应优先级为0
  • 中断7(外部中断1)的抢占优先级为2,响应优先级为0。

  那么这3个中断的优先级顺序为:中断7>中断3>中断6

五、定时器

  STM32的高级定时器TIM1, TIM8以及通用定时器TIM9, TIM10, TIM11的时钟来源是APB2总线;通用的 TIM2、TIM3、TIM4和TIM5 定时器 挂载在低速的APB1总线上。

5.1 F103定时器时钟疑问

疑问: 以F103为例,APB1 提供时钟:他的最大值是 36M。有很多人不理解,为什么 TIM2 的时钟不是 36M 而是 72M呢?

解答:通用定时器(TIM2-7)的时钟不是直接来自APB1,而是通过APB1的预分频器以后才到达定时器模块


  APB1 的时钟最大只能是 36M,在 RCC 时钟配置的函数,也就是程序最开始初始化系统时钟到 72M(AHB)的时候,里面有

1
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

除以了 2,也就是在我们配置 AHB 为 72M 的时候 APB1 是 72/2=36M 。根据当APB1的预分频系数为1 时,这个倍频器不起作用,定时器的时钟频率等于 APB1 的频率;当 APB1 的预分频系数为其它数值(即预分频系数为 2、4、8 或 16)时,这个倍频器起作用,定时器的时钟频率等于 APB1 的频率两倍。这里我们设置的是 2,那么到定时器就要乘以 2,那么就是 72M 了。

  综上,通用定时器的时钟最大为72MHz,挂在APB1上除了通用定时器的其他外设时钟最大为36MHz。这样设计的目的就是让挂在APB1上的低速外设有合适的低速时钟,同时还可以让挂在APB1上的通用定时器能够在不影响低速外设的情况下仍然有高速的时钟。
(注:系统时钟初始化后,默认AHB是72MHz,APB1是AHB的2分频36MHz,通用定时器时钟是APB1的两倍72MHz)

5.2 定时器计时算法

  定时器跟时间相关的量有:系统时钟、分频系数、重装载值。

  • 系统时钟: 默认不配置的时候是72M,可以根据自己需求配置;
  • 分频系数: psc,就是对系统时钟进行多少分频之后在使用,最好设置为72的倍数,方便运算;
  • 重新装载值: arr,是计算这么多值,时间到了之后重新开始计算的值,每一次计数的时间为分频之后时钟的到时;

举例:假设系统时间72M,分频系数设置为7200-1,那现在定时器的时钟为10kHz,每计一个数花费1/(10000)秒,重装值设置为5000-1,那一次溢出的时间为500ms。

Time = ((period+1)*(prescaler+1))/sysclock

  • time:溢出时间(MHz)
  • sysclock:系统时钟(us)
  • period:重装值
  • prescaler:分频系数

time = ((4999+1)*(7199+1))/72 = 5000 000us = 500ms


Ps:定时器的重装值不能为0。

  以上面为例:不改动分频系数的情况下,重装值设置为5000-1,那一次溢出的时间为500ms。如果让溢出的时间为1ms,那重装值要设置为10-1;但是无法设置溢出时间为0.1ms,重装值不能为1-1。该最小的定时溢出时间为2-1,即2ms触发一次定时器中断。

5.3 计时拓展

  定时器的计时可以采用 任务机制回调;实现思路如下:

  1. 创建结构体数组,用于存放 多定时器任务 数据;结构体内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /*******************************************************************************
    块注释:结构体定义声明
    *******************************************************************************/
    typedef struct{
    volatile uint32_t Time_task_name; //任务名称(组名称或上ID号码)
    volatile uint32_t Time_Out; //超时时间
    volatile uint32_t Time_Count; //任务启动为止到当前的时间
    FUNCTION* callback_F; //回调函数
    }TIMER_TASK; //定义添加执行任务所需要的数据
  2. 创建任务需要:任务名称,设定时间,回调函数名;

  3. 定时器中断会循环检索任务的存在;如果任务存在则对应的Time_Count(任务计时)++;如果Time_Count和Time_Out(设定时间)相同,则重置Time_Count,并执行回调;

  4. 回调函数一般短小精悍,因为是在中断内检索处理;用于置某些标志位或者关闭自身定时器任务

实现具体细节如下(以函数为划分):

  • 创建某个定时器任务
  • 复位某个定时器任务
  • 清除某个定时器任务(一般在执行回调函数中使用)
  • 清除全体定时器任务
  • 获取当前定时器计数(其值为该函数返回值)

六、串口通讯

  这里讲串口常见概念和现象。

6.1 TXE 和 TC

  串口的发送TX标志位 USART_FLAG_TXEUSART_FLAG_TC的理解:

  1. TXE是指“手里要搬运的”空;
  2. TC 是指“地上要搬运的”空;
  • USART_FLAG_TXE 来说,只是说明数据寄存器中的数据已经被发送移位寄存器取走了
  • USART_FLAG_TC来说,没必要每次当发送移位寄存器中的数据发送完成后都发生中断,而应该是整个串口数据帧全部发送完毕,包括最后一个字节也发送出去之后才应该开中断,这代表的就是一个数据帧发送完成事件了。

6.2 不停进入串口中断的Bug

  Bug现象:在使用stm32的时候,发现usart会莫名的卡在串口中断里,然而串口初始化只配置了RXNE中断,打断点发现不断进入中断却发现不是RXNE中断引起的

该Bug来源:经过查找资料发现是ORE的问题:开启RXNE中断同时, ORE 也会被开启;

但是如果直接用 USART_GetITStatus 无法读取到ORE标志位 置位的信息,这样也就无法消除中断申请(不知道什么时候置位),一直进入串口中断;因为我们没有使能ORE标志位,所以才读不到 置位 信息;故

1
USART_ITConfig(USART1, USART_IT_ORE, ENABLE);

然后在串口中断内做对应处理:

1
2
3
4
5
//注意!不能使用if(USART_GetITStatus(_UART_M, USART_IT_RXNE) != RESET)来判断
if(USART_GetITStatus(_UART_M,USART_IT_ORE) != RESET)
{
USART_ClearITPendingBit(_UART_M,USART_IT_ORE);
}

STM32中文参考手册541页,内容如下:

ORE:过载错误 (Overrun error)

  当RXNE仍然是’1’的时候,当前被接收在移位寄存器中的数据,需要传送至RDR寄存器时,硬件将该位置位。如果USART_CR1中的RXNEIE为’1’的话,则产生中断。由软件序列将其清零(先读USART_SR,然后读USART_CR)。

  • 0:没有过载错误;
  • 1:检测到过载错误。
    注意:该位被置位时, RDR寄存器中的值不会丢失,但是移位寄存器中的数据会被覆盖。如果设置了EIE位,在多缓冲器通信模式下,ORE标志置位会产生中断的

  关于ORE的更细致处理方法主要还是参照下面几个博主的文章:

STM32串口中断卡死主循环问题分析
关于USART接收中断的BUG和注意事项
关于STM32不断进入串口中断的问题

-------------本文结束感谢您的阅读-------------