跳到主要内容位置

HAL库基础

2.1 创建基础模板#

项目流程#

  • 确定需求与指标
  • 硬件开发
    • 芯片选型、器件选型等
    • 设计原理图
    • 设计PCB
    • 打样贴片,硬件测试
  • 软件开发
    • 代码准备(在选型完芯片后,就可以同步进行),搭建开发环境,熟悉基本功能
    • 调试(包括各个模块,业务功能)
  • 测试(测试人员进行的)
  • 交付量产
    • 硬件源文件(原理图、PCB、BOM表等)
    • 软件源文件(开发板代码、上位机等)
    • 测试文档
    • 量产
  • 产品发布

使用CubeMX创建F103工程模板#

这一部分的工作属于代码准备阶段。

引脚配置#

使用正点原子精英版,按键、LED、USART3不同,USART1、OLED、电机引脚相同,具体如下:

功能引脚
LED0PB5
LED1PE5
KEY0PE4
KEY1PE3
OLED SCLPF10
OLED SDAPF11
STEP_IN1PF1
STEP_IN2PF2
STEP_IN3PF3
STEP_IN4PF4
USART1_TXPA9
USART1_RXPA10
USART3_TXPB10
USART3_RXPB11

2.2 LED与KEY#

LED#

void Led0_On(void);
void Led0_Off(void);
void Led1_On(void);
void Led1_Off(void);
void led0_Shine(void);
void led1_Shine(void);
#include "driver_led.h"
#include "main.h"
void Led0_On(void)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
}
void Led0_Off(void)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
}
void Led1_On(void)
{
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
}
void Led1_Off(void)
{
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
}
void led0_Shine(void)
{
HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
HAL_Delay(500);
}
void led1_Shine(void)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
}

KEY#

#ifndef __DRIVER_KEY_H
#define __DRIVER_KEY_H
#include "main.h"
#define K0 HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin)
#define K1 HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin)
uint8_t K0_Value(void);
uint8_t K1_Value(void);
#endif
#include "driver_key.h"
uint8_t K0_Value(void)
{
if(K1 == 0)
{
HAL_Delay(10);
if(K1 == 0)
{
while(K1 == 0); /* 等待按键松开 */
return 0; /* 按键按下了 */
}
}
return 1;
}
uint8_t K1_Value(void)
{
if(K1 == 0)
{
HAL_Delay(10);
if(K1 == 0)
{
while(K1 == 0); /* 等待按键松开 */
return 0; /* 按键按下了 */
}
}
return 1;
}

2.3 OLED屏幕#

I2C协议#

参考资料:

  • i2c_spec.pdf
硬件连接#

I2C在硬件上的接法如下所示,主控芯片引出两条线SCL,SDA线,在一条I2C总线上可以接很多I2C设备,我们还会放一个上拉电阻(放一个上拉电阻的原因以后我们再说)。

image-20210220144722044

传输数据类比#

怎么通过I2C传输数据,我们需要把数据从主设备发送到从设备上去,也需要把数据从从设备传送到主设备上去,数据涉及到双向传输。

举个例子:

image-20210220145618978

体育老师:可以把球发给学生,也可以把球从学生中接过来。

  • 发球:

    • 老师:开始了(start)
    • 老师:A!我要发球给你!(地址/方向)
    • 学生A:到!(回应)
    • 老师把球发出去(传输)
    • A收到球之后,应该告诉老师一声(回应)
    • 老师:结束(停止)
  • 接球:

    • 老师:开始了(start)
    • 老师:B!把球发给我!(地址/方向)
    • 学生B:到!
    • B把球发给老师(传输)
    • 老师收到球之后,给B说一声,表示收到球了(回应)
    • 老师:结束(停止)

我们就使用这个简单的例子,来解释一下IIC的传输协议:

  • 老师说开始了,表示开始信号(start)
  • 老师提醒某个学生要发球,表示发送地址和方向(address/read/write)
  • 同学听到自己的名字了要回应(ACK)
  • 老师发球/接球,表示数据的传输
  • 收到球要回应:回应信号(ACK)
  • 老师说结束,表示IIC传输结束(P)
IIC传输数据的格式#

1 写操作

流程如下:

  • 主芯片要发出一个start信号
  • 然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)
  • 从设备回应(用来确定这个设备是否存在),然后就可以传输数据
  • 主设备发送一个字节数据给从设备,并等待回应
  • 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
  • 数据发送完之后,主芯片就会发送一个停止信号。

下图:白色背景表示"主→从",灰色背景表示"从→主"

image-20210220150757825

2 读操作

流程如下:

  • 主芯片要发出一个start信号

  • 然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)

  • 从设备回应(用来确定这个设备是否存在),然后就可以传输数据

  • 从设备发送一个字节数据给主设备,并等待回应

  • 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。

  • 数据发送完之后,主芯片就会发送一个停止信号。

下图:白色背景表示"主→从",灰色背景表示"从→主"

image-20210220150954993

3 I2C信号

I2C协议中数据传输的单位是字节,也就是8位。但是要用到9个时钟:前面8个时钟用来传输8数据,第9个时钟用来传输回应信号。传输时,先传输最高位(MSB)。

  • 开始信号(S):SCL为高电平时,SDA山高电平向低电平跳变,开始传送数据。
  • 结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
  • 响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA
  • SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化

I2C协议信号如下:

image-20210220151524099

4 协议细节

  • 如何在SDA上实现双向传输? 主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引脚/接受引脚)。

  • 主、从设备都可以通过SDA发送数据,肯定不能同时发送数据,怎么错开时间? 在9个时钟里, 前8个时钟由主设备发送数据的话,第9个时钟就由从设备发送数据; 前8个时钟由从设备发送数据的话,第9个时钟就由主设备发送数据。

  • 双方设备中,某个设备发送数据时,另一方怎样才能不影响SDA上的数据? 设备的SDA中有一个三极管,使用开极/开漏电路(三极管是开极,CMOS管是开漏,作用一样),如下图: image-20210220152057547

真值表如下: image-20210220152134970

从真值表和电路图我们可以知道:

  • 当某一个芯片不想影响SDA线时,那就不驱动这个三极管
  • 想让SDA输出高电平,双方都不驱动三极管(SDA通过上拉电阻变为高电平)
  • 想让SDA输出低电平,就驱动三极管

从下面的例子可以看看数据是怎么传的(实现双向传输)。 举例:主设备发送(8bit)给从设备

  • 前8个clk

    • 从设备不要影响SDA,从设备不驱动三极管
    • 主设备决定数据,主设备要发送1时不驱动三极管,要发送0时驱动三极管
  • 第9个clk,由从设备决定数据

    • 主设备不驱动三极管
    • 从设备决定数据,要发出回应信号的话,就驱动三极管让SDA变为0
    • 从这里也可以知道ACK信号是低电平

从上面的例子,就可以知道怎样在一条线上实现双向传输,这就是SDA上要使用上拉电阻的原因。

为何SCL也要使用上拉电阻? 在第9个时钟之后,如果有某一方需要更多的时间来处理数据,它可以一直驱动三极管把SCL拉低。 当SCL为低电平时候,大家都不应该使用IIC总线,只有当SCL从低电平变为高电平的时候,IIC总线才能被使用。 当它就绪后,就可以不再驱动三极管,这是上拉电阻把SCL变为高电平,其他设备就可以继续使用I2C总线了。

对于IIC协议它只能规定怎么传输数据,数据是什么含义由从设备决定。

I2C底层驱动#

OLED模块的电路:SDA/SCL都通过上拉电阻拉高

实现I2C驱动:

  • 可以发现除了开始和结束信号,其他操作时都是先拉低SCL,再操作SDA

2.4 SSD1306的I2C数据格式与显存访问#

SSD1306的特点:

128×64点矩阵面板; 有256阶对比度可调节; 支持6800/8080并行总线; 支持SPI、I2C串行总线; 支持水平方向和垂直方向的滚动; 支持行或列的重映射;

  • 设备地址: 第0位→读写位;→‘1’表示读;‘0'表示写 第1位→从机地址位,→‘DC#引脚决定是0还是1 第2-7位→固定值0b011110; 所以从机地址是0x7A(写地址),0x7B(读地址)
  • 数据格式:

​ 示例: ​

  • 显示方式:数据写入显存,会一一对应到像素点上
  • 地址模式: 使用的是页地址模式:往显存里面写入数据后,列地址指针会自动递增1。设置好起始页和起始列之后,就可以连续发送数据,而不用每发送一个数据就去指定一个页和列的地址了,但是如果列地址指针递增到了设置的结束列地址,那么列地址指针就会复位回到设置的起始列地址,而页地址指针是不会有变化的。 因此为了访问下一页显存中的内容,用户必须设置新的页和列的起始地址。

2.5 OLED驱动程序#

这部分参考SSD1306手册8.1.5 i2c / 9 CommandTable,源码的driver_oled.cdriver_oled.h,添加了详细注释,主要实现:

  • 基础写命令,写数据,写n个数据的函数
  • 实现Fundamental Command Table,基础命令表(对比度、全屏点亮或者熄灭、阴码显示或者阳码显示、打开显示或者关闭显示),后面三个通过宏定义实现
  • Scrolling Command Table,滚动命令表格(水平滚动、连续的垂直和水平滚动、开始滚动、停止滚动、垂直滚动的区域)
  • 地址设置功能函数(设置OLED在页地址模式下的显示起始行地址、地址模式、在水平地址模式或垂直地址模式下像素显示的起始行地址和结束行地址等)
  • 硬件配置功能函数(设置OLED从第line行开始显示、设置OLED、COM引脚的扫描方向、显示垂直偏移、COM引脚属性)

1.复用率:(Multiplex Ratio,简称Mux Ratio)是OLED显示屏的一个重要参数,它定义了屏幕中操作的共阳极或共阴极线路的数目。复用率与显示屏上能够显示的最大行数密切相关。例如,如果复用率被设定为 1:64,这意味着共阳线或共阴线中的每一条线会依次与每个像素连接 64 次以刷新整个显示屏。该比率值通常在 15 到 63 的范围之内,这是SSD1306控制器对于复用率的典型设置范围,对应着不同大小的OLED屏最大行数。设置复用率,改变整个屏幕的显示行数

2.在SSD1306或其他类似的OLED屏幕控制器中,COM引脚通常指的是屏幕的共用端(COMmon)引脚。这些引脚通常与屏幕的多个行(横向)相关联,它们负责驱动显示屏幕上的像素。 COM引脚属性-连续的/可选择的:顺序模式指的是COM引脚与屏幕行的连接是一对一的,而交替模式意味着连接是交叉的,这通常会影响显示内容的布局和屏幕的驱动方式。 COM引脚属性-翻转与否:是否将COM扫描方向翻转,即是否将屏幕内容上下翻转

  • 时间设置功能函数(扫描周期和晶振频率、预充电周期、电压等级)

1.预充电周期

预充电周期,通常分为两个阶段,是OLED像素从关闭状态转换至选通(或全亮状态)所需的时间。该周期影响着OLED的显示性能和像素的点亮速度。

  • Phase1:第一阶段的预充电时间,通常用于充电至像素节点,以便提供像素点亮所需的电荷。
  • Phase2:第二阶段是为了平衡电荷,并使得像素能够快速达到最终亮度,以确保快速、均匀的像素响应时间。

通过修改预充电周期的阶段时间,可以改变像素点亮前的准备时间,进而影响显示屏幕的功耗和显示亮度。较短的预充电周期可能使屏幕响应变快,但可能会导致像素亮度不足或增加屏幕的功耗。较长的预充电周期可以提高显示亮度和屏幕稳定性,但会造成较慢的像素响应时间。

2.电压等级

V_COMH 表示共用端(COM端)的电压水平,通常称为"COM Pin Voltage",它是OLED屏幕的一个重要电气参数,对显示对比度、亮度和屏幕寿命都有直接影响。

  • 设置正确的 V_COMH 电压等级能确保像素能更准确地打开和关闭。
  • V_COMH 电压等级通常有几个预设的值可以选择,越高的设置可以得到越高的对比度,但可能会增加功耗,并有可能缩短OLED面板的寿命。

SSD1306有三个等级:0.65v, 0.77v, 0.83v

  • 电荷碰撞功能函数(打开或关闭OLED的电荷泵)

基础功能函数#

1.初始化函数:按照手册初始化流程图设置OLED

2.驱动OLED需要先设置起始的列地址与页地址(页地址模式下),封装一个函数:

/************** 8. 基本驱动功能函数 **************/
/*
* 函数名:OLED_SetPosition
* 功能描述:设置像素显示的起始页和起始列地址
* 输入参数:page-->页地址模式下的起始页地址
* col-->页地址模式下的起始行地址
* 输出参数:无
* 返回值:无
*/
void OLED_SetPosition(uint8_t page, uint8_t col)
{
OLED_SetPageAddr_PAGE(page);
OLED_SetColAddr_PAGE(col);
}
//调用
OLED_Init();
OLED_SetPosition(0, 0);//设置第0页第0列
OLED_WriteData(0xff);

正常情况屏幕为下图编号:

通过设置:

OLED_SEG_REMAP(); // 4. 行翻转
OLED_SCAN_REMAP(); // 5. 正常扫描

可以将屏幕翻转到左上方。

3.显示一个字符

需要字模。通过软件生成字库。

然后按传入的char字符获得字模。我们选取的字模大小是8*16的,所以需要两页上分别显示上下部分

/*
* 函数名:OLED_PutChar
* 功能描述:显示一个字符
* 输入参数:page --> 起始页地址
* col --> 起始列地址
* c --> 显示的字符
* 输出参数:无
* 返回值:无
*/
void OLED_PutChar(uint8_t page, uint8_t col, char c)
{
OLED_SetPosition(page, col);
OLED_WriteNBytes((uint8_t*)&ascii_font[c][0], 8);
OLED_SetPosition(page + 1, col);
OLED_WriteNBytes((uint8_t*)&ascii_font[c][8], 8);
}

打印字符串:每个字符列地址每次+=8,超出127后,页地址+=2

/*
* 函数名:OLED_PrintString
* 功能描述:显示一个字符串
* 输入参数:page --> 起始页地址
* col --> 起始列地址
* str --> 显示的字符串
* 输出参数:无
* 返回值:无
*/
void OLED_PrintString(uint8_t page, uint8_t col, char *str)
{
while(*str != 0)
{
OLED_PutChar(page, col, *str);
col += 8;
if(col > 127)
{
page += 2;
}
if(page > 7)
{
page = 0;
}
str++;
}
}

4.补充

OLED_SetComConfig(COM_PIN_ALT, COM_NOREMAP); //使用可选择的方式,可以优化显示的布局内容,看上去更小更美观一些

功耗与休眠

  • 合理配置OLED的参数;
  • 合理分配显示时间,考虑是否需要一直显示;考虑设计熄灭和显示的周期;
  • 模块的休眠与MCU的休眠

2.6 通讯#

通讯的基本概念#

同步通讯与异步通讯:

比特率:系统在单位时间内传输的比特位(二进制0或1)个数,通常用Rb表示,单位是比特/秒(bit/s),缩写为bps

波特率:系统在单位时间内传输的码元个数,通常用RB表示,单位是波特(Bd)

码元有N个状态时,比特率与波特率的关系式:Rb=RB×log2NR_b=R_B×log_2N。比如串口的状态只有0或1,因此N=2,比特率和波特率就相等

串口原理图#

USART1_RX PA10

USART1_TX PA9

在正点原子的精英版,WIFI模块的串口USART3没有重映射到别的引脚。TX-PB10,RX-PB11

USART的收发控制流程如下图:控制寄存器负责收发使能、中断使能以及收发状态寄存器来显示当前收发状态;波特率配置寄存器配置数据的波特率;移位寄存器分为发送和接收移位寄存器,发送移位寄存器将放入到TDR中的数据一位一位的发送出去,接收移位寄存器则相反。

USART初始化与收发#

对应usart.c中的初始化函数MX_USART1_UART_Init

UART常用HAL库函数与结构体

描述UART外设的结构体USART_TypeDef

typedef struct
{
__IO uint32_t SR; /*!< USART Status register, Address offset: 0x00 */
__IO uint32_t DR; /*!< USART Data register, Address offset: 0x04 */
__IO uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */
__IO uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */
__IO uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */
__IO uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */
__IO uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;

编写driver_usart.c

void DebugPrint(const char *str)
{
uint16_t len = strlen(str);
HAL_UART_Transmit(&huart1, (uint8_t *)str, len, 300);
}
void DebugGet(char *str, uint16_t len)
{
while(HAL_UART_Receive(&huart1, (uint8_t *)str, len, 300) != HAL_OK);
}

重定向

/*---------重定向----printf/scanf---putchar/getchar-------*/
extern UART_HandleTypeDef huart1;
// fputc函数将字符ch发送到指定的文件流f中。忽略文件流f,并将字符发送到UART端口。
int fputc(int ch, FILE *f)
{
// 如果USART发送忙,等待发送完成
while(HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 300) != HAL_OK);
return ch;
}
// fgetc函数从文件流f中读取一个字符。在此例中,忽略文件流f,并从UART端口读取字符。
int fgetc(FILE *f)
{
uint8_t c = 0;
// 如果USART接收忙,等待接收完成
while(HAL_UART_Receive(&huart1, &c, 1, 300) != HAL_OK);
return c;
}

编写main.c

while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// DebugGet((char *)buf, 4);
// DebugPrint((char *)buf);
// memset(buf, 0, 4);
// scanf("%s", buf);
// printf("%s", buf);
// memset(buf, 0, strlen((char *)buf));
buf[0] = getchar();
switch(buf[0])
{
case 'A':
{
Led0_On();
printf("LED0 ON.\r\n");
break;
}
case 'a':
{
Led0_Off();
printf("LED0 OFF.\r\n");
buf[0] = 0;
break;
}
default:
break;
}
}
/* USER CODE END 3 */

中断系统与HAL库#

STM32优先级:

  • 抢占优先级:抢占优先级可以实现中断嵌套,抢占优先级级数低的可抢占级数高的
  • 子优先级:无法实现中断嵌套,同一时刻两个子优先级不同的中断来临,则先处理优先级高的即优先级级数低的中断;如果先后发生,则先处理上一个中断,再处理后面的中断。

F103_Usart_irq有问题,重新拷贝F103_Usart添加driver_usart.c/.h

问题解决:重定向fput/fget需要勾选microLIB库

程序基本逻辑:

  • 设置中断优先级,使能串口的发送完成中断和接收寄存器非空中断

    /*
    * 函数名:EnableDebugIRQ
    * 功能描述:使能USART1的中断
    * 输入参数:无
    * 输出参数:无
    * 返回值:无
    */
    void EnableDebugIRQ(void)
    {
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 1); // 设置USART1中断的优先级
    HAL_NVIC_EnableIRQ(USART1_IRQn); // 使能USART1的中断
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_TC | UART_IT_RXNE); // 使能USRAT1的发送和接收中断
    }
    /*
    * 函数名:DisableDebugIRQ
    * 功能描述:失能USART1的中断
    * 输入参数:无
    * 输出参数:无
    * 返回值:无
    */
    void DisableDebugIRQ(void)
    {
    __HAL_UART_DISABLE_IT(&huart1, UART_IT_TC | UART_IT_RXNE); // 失能USRAT1的发送和接收中断
    HAL_NVIC_DisableIRQ(USART1_IRQn); // 失能USART1的中断
    }
  • 重定向,定义fgetc/fputc

    /*
    * 函数名:fputc
    * 功能描述:printf/putchar 标准输出函数的底层输出函数
    * 输入参数:ch --> 要输出的数据
    * 输出参数:无
    * 返回值:无
    */
    int fputc(int ch, FILE *f)
    {
    txcplt_flag = 0;
    HAL_UART_Transmit_IT(&huart1, (uint8_t*)&ch, 1);
    while(txcplt_flag==0);
    }
    /*
    * 函数名:fgetc
    * 功能描述:scanf/getchar 标准输出函数的底层输出函数
    * 输入参数:
    * 输出参数:无
    * 返回值:接收到的数据
    */
    int fgetc(FILE *f)
    {
    char c = 0;
    rxcplt_flag = 0;
    HAL_UART_Receive_IT(&huart1, (uint8_t*)&c, 1);
    while(rxcplt_flag==0);
    return c;
    }
  • 重定义串口中断处理函数

    /*
    * 函数名:USART1_IRQHandler
    * 功能描述:USART1的中断服务函数
    * 输入参数:无
    * 输出参数:无
    * 返回值:无
    */
    void USART1_IRQHandler(void)
    {
    HAL_UART_IRQHandler(&huart1); // HAL库中的UART统一中断服务函数,通过形参判断是要处理谁的中断
    }
    /*
    * 函数名:HAL_UART_RxCpltCallback
    * 功能描述:HAL库中的UART接收完成回调函数
    * 输入参数:huart --> UART的设备句柄,用以指明UART设备是哪一个UART
    * 输出参数:无
    * 返回值:无
    */
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
    if(huart->Instance == USART1) // 判断进来的是否是USART1这个UART设备
    {
    rxcplt_flag = 1; // 进入此回调函数表明接收指定长度的数据已经完成,将标志置一
    }
    }
    /*
    * 函数名:HAL_UART_TxCpltCallback
    * 功能描述:HAL库中的UART发送完成回调函数
    * 输入参数:huart --> UART的设备句柄,用以指明UART设备是哪一个UART
    * 输出参数:无
    * 返回值:无
    */
    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
    {
    if(huart->Instance == USART1)
    {
    txcplt_flag = 1; // 进入此回调函数表明发送指定长度的数据已经完成,将标志置一
    }
    }

    当串口接收到数据,寄存器的变化:

外部中断#

F103的外部中断结构图如下:

外部中断线一共有20条:

外部中断配置寄存器注意-同一时刻只能配置某一个引脚的一组GPIO的外部中断,即配置PA0就不能配置PB0-PG0了,后面配置的会覆盖前面的

外部中断函数:

编程:

  • 配置GPIO,设置中断优先级和使能中断

    void KEY_GPIO_ReInit(void)
    {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    KEY0_GPIO_CLK_EN();
    KEY1_GPIO_CLK_EN();
    GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING; // 双边沿触发中断
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Pin = KEY1_PIN;
    HAL_GPIO_Init(KEY1_PORT, &GPIO_InitStruct);
    GPIO_InitStruct.Pin = KEY0_PIN;
    HAL_GPIO_Init(KEY0_PORT, &GPIO_InitStruct);
    HAL_NVIC_SetPriority(EXTI3_IRQn, 0, 2);
    HAL_NVIC_EnableIRQ(EXTI3_IRQn);
    HAL_NVIC_SetPriority(EXTI4_IRQn, 0, 2);
    HAL_NVIC_EnableIRQ(EXTI4_IRQn);
    }
  • 编写中断处理函数,中断处理过程如下图

    void EXTI3_IRQHandler(void)
    {
    HAL_GPIO_EXTI_IRQHandler(KEY0_PIN);
    HAL_GPIO_EXTI_IRQHandler(KEY1_PIN);
    }
    void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
    {
    if(KEY0_PIN == GPIO_Pin)
    {
    }
    else if(KEY1_PIN == GPIO_Pin)
    {
    }
    }

2.7 FreeRTOS#

1. 目的#

​ 本篇是介绍在5_4_Serialport_RingBuffer_GPIO_IRQ工程基础上创建一个带有RTOS的工程模板,使用的工具是STM32CubeMX.

2. 步骤#

​ 首先将5_4_Serialport_RingBuffer_GPIO_IRQ复制改名为6_FreeRTOS_Template,随后打开里面的STM32CubeMX配置文件F103_Moduel.ioc,打开这个文件有可能会因为STM32CubeMX版本不一致弹出如下信息:

选择Migrate

2.1 选择CMSIS的RTOS接口版本#

CMSIS封装的RTOS接口有两个版本RTOS V1RTOS V2,V2兼容V1,支持更多的cortex内核,我们的课程使用的就是V2版本的接口。

​ 在STM32CubeMX配置界面的Middleware栏选择FREERTOS,在Interface那里选择CMSIS_V2,其它的参数我们暂且使用默认值,如图所示:

2.2 改变HAL库使用的时基源#

​ 首先说明下为什么要改变HAL库使用的时基源。在默认情况下,ST的HAL库使用的时钟基准输入源也就是时基源,利用的是内核的滴答定时器,如果没有使用RTOS的话没有什么问题;但是如果要使用一个RTOS比如FreeRTOS或者RT-Thread,这些RTOS的内核时钟通常也是利用的内核的滴答定时器,为了不影响内核的运行,HAL库的时基源就最好换一个,比如我们这里就选择STM32F103的定时器TIM8作为HAL库的时基源:

在实际设计中,选择哪一个定时器作为HAL库的时基源应该需要谨慎考虑,为了不要互相影响,最好不要选择与控制其它外设的定时器相同的定时器,比如我需要使用TIM3来输出一个PWM波,那么这里就最好不要再选择TIM3作为HAL库的时基源。

2.3 确认IDE的版本#

​ 在生成工程前,我们提供的cubemx选择的IDE版本可能和自己实际使用的IDE版本不同,因而就需要注意一下IDE的版本问题。比如这个cubemx配置工程我们一开始选择的是MDK,版本是5.27,但是电脑上使用的MDK实际已经更新到更高等级的版本了,那么这里最好也要改变:

2.4 生成工程#

​ 经过前面3步的配置后,就可以点击Generate Code生成工程了。

2.5 MDK工程勾选MicroLIB#

​ 因为我们是在5_4_Serialport_RingBuffer_GPIO_IRQ基础上修改的,源工程的MDK是需要勾选MicroLIB来支撑运行的,而使用STM32CubeMX配置文件改变配置重新生成后的工程默认是不会勾选此项的,所以我们需要打开重新生成的工程,把这里勾选上:

2.6 简单修改freertos.c#

​ 在STM32CubeMX中选择好RTOS的接口版本后,默认参数中是有一个默认的任务的:

为了验证我们配置的RTOS是否能够正常运行,我们可以在freertos.c中添加测试打印(5_4工程已经实现了printf功能)

然后我们编译工程,烧写到开发板上看下结果.

可以看到能够正常打印,说明RTOS内核已经运行起来了,这样就得到了我们的FreeRTOS的工程模板。


请点击左侧菜单(移动端为右下角)选择要查看的所有笔记吧。