基础点
NRST:复位引脚
OSC_IN:时钟引脚(晶振)
SYSCLK:系统时钟
SDIO:SD(内存)卡
FSMC:外扩内存
AHB:总线时钟
RTC:实时时钟 在断电情况下 RTC仍可以独立运行 只要芯片的备用电源一直供电,RTC上的时间会一直走(一般用外部低速时钟,32.768kHz)
Peripherals
:外设
单片机读操作
寄存器置位
置位 |= (0x01<<n) 清零&=
整个芯片的时钟源:
HSE 外部高速时钟(晶振) HSI 内部高速时钟(RC 电路)
LSE 外部低速时钟(晶振) LSI 内部低速时钟(RC 电路)
系统时钟源: HSI HSE PLL 一般情况下,芯片选择 HSE. 系统选择 PLL 作为时钟源HSE(8M)--倍频(*9)--PLL(72M)--SYSCLK(72M)--AHB(72M)
--APB 分频 -- APB1(36M)/APB2(72M)-- 不同的外设
SW/JTAG:下载方式
flash:存储代码
DMA:即直接存储器访问
总线:Pbus Ibus Dbus
两种时钟:APB1ENR
、APB2ENR
APB1
外设时钟使能寄存器(RCC_APB1ENR) 低速APB使能,最大允许频率36MHz
APB2
外设时钟使能寄存器(RCC_APB2ENR) 高速APB使能,最大允许频率72MHz GPIO串口
时钟修改
在头文件中转至定义,在119行进行设置
倍频
:
在时钟文件system_stm32f10x.c中的262行SetSysClock();
选中,然后跳转(f12)到432行处SetSysClockTo72();
再次选中然后跳转(f12)到1053行----1056行进行设置
设置倍频的倍数!
GPIO的8种工作模式详解
-
GPIO浮空输入_IN_FLOATING模式
-
GPIO带上拉输入_IPU 模式
-
GPIO带下拉输入_IPD 模式
-
GPIO模拟输入_AIN 模式
-
GPIO开漏输出_OUT_OD 模式
-
GPIO推挽输出_OUT_PP模式
-
GPIO复用开漏输出_AF_OD模式
-
GPIO复用推挽输出_AF_PP模式
库函数创建使用
1.首先在keil工程中新建一个组
2.打开固件库文件夹
3.将这个文件复制工程中的user文件夹中
4.打开固件库文件夹
5.复制此文件夹到工程中
6.在keil软件中,添加.c和.h文件。此时会报错,找到#include "stm32f10x.h"转到定义,大概在105行,复制USE_STDPERIPH_DRIVER到魔法棒中的c/c++中的Preprocessor Symbols下的define。
中断
打断并保存主程序的运行,转而执行其他紧急事件,结束后重新执行主程序
中断处理流程:
循序 -- 事件产生 -- 现场保护(数据入栈)-- 执行中断服务函数 -- 继续执行程序
中断源:中断发生的事件
抢占/占先:可以打断其他程序的执行
次级/响应:抢占相同时,当多个终端同时发生,次级决定哪个先执行。
ST使用高四位设置优先级
-
io口对应中断线
-
设置触发的边缘
-
设置是否允许中断 (外部中断)EXTI:
-
设置分组
-
设置优先级
-
设置使能中断通道 NVIC的作用:步骤4,5,6
-
编写中断服务函数
RTC时钟配置
时钟源,一般由外部低速晶振
1.设置寄存器
SysTick定时器
编写使用SysTick定时器的步骤如下:
-
配置SysTick定时器的时钟源和计数器初值:首先,确定SysTick定时器的时钟源和计数器初值。SysTick定时器的时钟源可以是处理器时钟(通常为系统时钟)或外部时钟。计数器初值决定了定时器的定时周期。
-
启用SysTick定时器:调用
SysTick_Config
函数,将计数器初值作为参数传递给函数。这会配置并启用SysTick定时器,使其开始运行。 -
实现SysTick中断服务程序(SysTick ISR):在SysTick定时器达到计数器值为0时,会触发SysTick中断。您需要编写SysTick中断服务程序,即处理SysTick中断的代码。这段代码将在每次SysTick中断发生时执行。
-
在SysTick中断服务程序中实现所需的功能:在SysTick中断服务程序中,您可以实现所需的功能,例如更新计时器、执行特定任务或生成定时事件等。
启用SysTick定时器
SysTick定时器有4个寄存器分别为
CTRL | 控制及状态寄存器 |
---|---|
LOAD | 重装载数值寄存器 |
VAL | 当前数值寄存器 |
CALIB | 校准数值寄存器 |
在使用时只用设置前三个寄存器
1.手动编写
/*
函数功能:SysTick初始化
函数参数:无
函数返回值:无
*/
void SysTick_Init(void)
{
//1.使能计数器 使能中断 时钟源72m
SysTick->CTRL |= ((1<<0)|(1<<1)|(1<<2));
//2.连拍模式 N+1个脉冲触发一次中断
SysTick->LOAD = 72000 - 1;
//3.计数器清零
SysTick->VAL = 0;
//4.内核函数 --设置优先级
NVIC_SetPriority(SysTick_IRQn,0XF);//优先级
NVIC_EnableIRQ(SysTick_IRQn);
}
1.直接调用
void SysTick_Init(void)
{
SysTick_Config(72000-1);
}
SysTick_Config
函数用于配置和启用SysTick定时器,并设置其计数器的初值。该函数接受一个参数,即一个32位无符号整数。
参数值为0时,表示禁用SysTick定时器,即不启动定时器。
参数值为非零值时,表示启用SysTick定时器,并将该值作为计数器的初值。
例如,如果调用SysTick_Config(1000)
,将SysTick定时器的计数器初值设置为1000,并启用定时器。在每个时钟周期内,计数器值会减1,直到计数器值变为0,触发SysTick中断。
2.实现SysTick中断服务程序
在stm32f10x_it.c
文件里面最下面找到SysTick_Handler服务函数,在中断函数中处理
//中断服务函数 --每一毫秒产生一个中断
void SysTick_Handler(void)
{
runtime++;
}
3.编写延时函数
void Delay_ms(uint32_t time)
{
uint32_t tmp = runtime; //保存当前系统时间
while(tmp + time >runtime)
{
}
}
USART串口通信
有单独的发送器与接收器,由发送器和接收器控制,数据存放在发送寄存器与接收寄存器
数据发送:将数据填入数据寄存器,在时钟的驱动下自动从低位一位一位的往外发送。
检测标志:接收缓冲器状态 发送缓冲器状态 传输结束标志
错误检测标志:溢出 噪音(杂波) 帧错误 校验错误
10个带标志中断: 发送数据寄存器空 发送完成 接收寄存器满 检测到总线空闲(接收到一个数据后有一个字符的空闲时间)10位/波特率
数据接收:配置接收中断,中断服务程序中取数据。
数据发送: 首先判断发送寄存器是否为空,调用函数USART_GetFlagStatus(串口号,TC寄存器)
判断:为1的话,说明上次传输完成,为0(reset)的话传输未完成。
接收数据:借助接收中断USART_ITConfig(USART1,USART_IT_RXNE,ENABLE),来接受数据。
每接收一个字节产生一次中断。
//使能串口1的接收中断
void Usart1RxITInit(void)
{
//使能串口1的接收中断 和 空闲中断
USART_ITConfig(USART1,USART_IT_RXNE ,ENABLE);
USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);
NVIC_InitTypeDef NVIC_InitStruct={0};
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);
}
//编写中断服务函数(在启动文件中)
void USART1_IRQHandler(void)
{
u8 rxbuff=0;//接收到的数据 每接收一个字节产生一次中断。
u16 count=0;//接收到的数据的个数
u8 rxflag=0;//接收完成标志位
//先判断终端是否产生
//获取终端状态
if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)//接收中断产生
{
USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清中断
rxbuff[count++] = USART_ReceiveData(USART1); //保存串口助手发过来的数据
USART3->DR=rxbuff[count++];//或者用 USART_SendData(USART3,data);
}
//获取终端状态
if(USART_GetITStatus(USART1,USART_IT_IDLE)==SET)//空闲中断产生
{
USART1->DR;
count=0;
rxflag=1;
/*
清中断 --对于空闲中断清理 特殊在于:需要先读取 SR USART_GetITStatus(USART2,USART_IT_IDLE)这个语句相当于读取了 SR
在读DR 就可以了
*/
}
}
空闲中断的作用就是,为了接收不定长数据。
原因:接收不定长数据时,数据无法处理,不知道上次数据是否发送完成。
RS485
RS485收发器作用:TTL电平信号转为差分信号
半双工模式 主从
RE控制接收模式(低电平有效) DE 控制发送模式
- 主机的GPIO会控制RS-485收发器的DE管脚,设置发送模式,从TXD线向RS-485收发器的数据(D或DI)线发送一个字节,收发器将在A和B线上将单端UART位流转换为差分位流,数据离开收发器后,主机立即将收发器的模式切换为接收模式。
- 从机和主机是类似的,从机控制RS-485收发器的/RE管脚,设置为接收模式,接收主机发送的比特流,将其转换为单端信号,通过从机的UART RXD线接收,当从机准备好响应时,它按主机原来的方式进行发送,而主机变为接收。
电平状态:A > B 逻辑 ‘1’ 电平差+200mv
A < B 逻辑 ‘0’ 电平差-200mv
问题:
如何实现RS-485/422多点通讯
RS-485总线上任何时候只能有一发送器发送。半双工方式,主从只能一个发。全双工方式,主站总可发送,从站只能有一个发送。
#include "sys.h"
#include "rs485.h"
#include "delay.h"
#ifdef EN_USART2_RX //如果使能了接收
//接收缓存区
u8 RS485_RX_BUF[64]; //接收缓冲,最大64个字节.
//接收到的数据长度
u8 RS485_RX_CNT=0;
void USART2_IRQHandler(void)
{
u8 res;
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) //接收到数据
{
res =USART_ReceiveData(USART2); //读取接收到的数据
if(RS485_RX_CNT<64)
{
RS485_RX_BUF[RS485_RX_CNT]=res; //记录接收到的值
RS485_RX_CNT++; //接收数据增加1
}
}
}
#endif
//初始化IO 串口2
//pclk1:PCLK1时钟频率(Mhz)
//bound:波特率
void RS485_Init(u32 bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOD, ENABLE);//使能GPIOA,D时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能USART2时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //PD7端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //PA2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//PA3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2,ENABLE);//复位串口2
RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2,DISABLE);//停止复位
#ifdef EN_USART2_RX //如果使能了接收
USART_InitStructure.USART_BaudRate = bound;//波特率设置
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位数据长度
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;///奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//收发模式
USART_Init(USART2, &USART_InitStructure); ; //初始化串口
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; //使能串口2中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; //先占优先级2级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级2级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启中断
USART_Cmd(USART2, ENABLE); //使能串口
#endif
RS485_TX_EN=0; //默认为接收模式
}
//RS485发送len个字节.
//buf:发送区首地址
//len:发送的字节数(为了和本代码的接收匹配,这里建议不要超过64个字节)
void RS485_Send_Data(u8 *buf,u8 len)
{
u8 t;
RS485_TX_EN=1; //设置为发送模式
for(t=0;t<len;t++) //循环发送数据
{
while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
USART_SendData(USART2,buf[t]);
}
while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
RS485_RX_CNT=0;
RS485_TX_EN=0; //设置为接收模式
}
//RS485查询接收到的数据
//buf:接收缓存首地址
//len:读到的数据长度
void RS485_Receive_Data(u8 *buf,u8 *len)
{
u8 rxlen=RS485_RX_CNT;
u8 i=0;
*len=0; //默认为0
delay_ms(10); //等待10ms,连续超过10ms没有接收到一个数据,则认为接收结束
if(rxlen==RS485_RX_CNT&&rxlen)//接收到了数据,且接收完成了
{
for(i=0;i<rxlen;i++)
{
buf[i]=RS485_RX_BUF[i];
}
*len=RS485_RX_CNT; //记录本次数据长度
RS485_RX_CNT=0; //清零
}
}
此部分代码总共 4 个函数
- RS485_Init 函数为 485 通信初始化函数,其实基本上就是在配置串口 2,只是把 PD7 也顺带配置了,用于控制 SP3485 的收发。同时如果使能中断接收的话,会执行串口 2 的中断接收配置。
- USART2_IRQHandler 函数用于中断接收来自 485 总线的数据,将其存放RS485_RX_BUF 里面。
- RS485_Send_Data 和RS485_Receive_Data 这两个函数用来发送数据到 485 总线和读取从 485 总线收到的数据。
MUDBOS通信
PDU:与基础通信层无关的简单协议数据单元
ADU:特定总线或网络上 的 MODBUS 协议映射能够在应用数据单元
离散输入:一个地址一个数据位,用户只能读取它的状态,不能修改。比如面板上的按键、开关状态,电机的故障状态。
线圈(输出):一个地址一个数据位,用户可以置位、复位,可以回读状态,比如继电器输出,电机的启停控制信号。
输入寄存器:一个地址16位数据,用户只能读,不能修改,比如一个电压值的读数。
保持寄存器(输出):一个地址16位数据,用户可以写,也可以回读,比如一个控制变频器的电流值。
注意:
- 输入的信号:用户只能看不能改。
- 输出的信号:用户控制,并可以回读。
- 离散的数据只有一位,模拟的数据有16位。
应用层协议 请求/应答 主/从 类似IIC
IIC---同步串行半双工通信
同步
: 意味着数据在主设备和从设备之间通过共享的时钟信号进行传输。主从设备都会按照这个时钟信号进行同步,确保通信过程中数据的完整性。
串行
: 意味着数据一次只能以一位(bit)的形式在单条通信线(SDA - 串行数据线)上传输,既用于数据发送又用于数据接收。
半双工:
意味着通信可以在两个方向上进行(可以发送和接收数据),但不能同时进行。在IIC通信中,设备轮流在同一通信线上进行数据传输和接收。
主机 向从机中 写 数据
- 主机发送起始信号。
- 主机发送7位设备地址+一位0(表示写数据),到从机。
- 主机释放SDA信号线,等待从机的应答。
- 如果从机正确接收到数据,从机发送一个有效应答位(0)给主机,表示已收到数据。
- 主机收到从机的有效应答位后,发送寄存器地址。
- 主机释放SDA信号线,等待从机的应答。
- 如果从机正确接收到寄存器地址,从机发送一个有效应答位给主机。
- 主机收到从机的有效应答位后,发送要写入从机的数据。
- 从机正确接收到数据后,向主机发送一个有效应答位。
- 主机发送停止信号,结束传输过程。
主机 向从机中 读 数据
- 主机发送起始信号。
- 主机主机发送7位设备地址+一位0(表示写数据),到从机。
- 主机释放SDA信号线,等待从机的应答。
- 如果从机正确接收到数据,从机发送一个有效应答位(0)给主机,表示已收到数据。
- 主机收到从机的有效应答位后,发送要读取的寄存器地址。
- 主机释放SDA信号线,等待从机的应答。
- 如果从机正确接收到寄存器地址,从机发送一个有效应答位给主机。
- 主机再次发送起始信号。
- 主机主机发送7位设备地址+一位1(表示读数据) ,到从机。
- 主机释放SDA信号线,等待从机的应答。
- 如果从机正确接收到数据,从机发送一个有效应答位(0)给主机,表示已收到数据。
- 从机将要读取的寄存器中的数据通过SDA信号线发送给主机。
- 主机收到从机发送的数据后,向从机发送一个非应答信号(1)。
- 主机发送停止信号,结束传输过程。
代码编写
1.起始信号————停止信号
2.应答——非应答——检测应答信号
3.发送——读取信号
SPI-FLASH
4线串行同步全双工
主/从模式
支持8位或16位输出
SPI一般使用4条线通信分别是?
1、 MISO ————— 主设备数据输入、从设备数据输出
2、 MOSI ————— 主设备数据输出、从设备数据输入
3、 SCLK ————— 时钟信号、由主设备产生
4、 NSS(CS) ————– 从设备片选信号,由主设备控制
W25Q64
空间结构: 容量8MB
有128块,一个块有64KB;每块16扇区。
一扇有16页 4KB(4096字节)
一页 256 字节
字 * 4 = 字节
8bit(比特位)= 1Byte(字节)
1024Byte(字节)= 1K(千字节)
1024K(千字节)= 1M(兆字节)
二、FLASH的存储特性
1、写入之前必须先擦除;
2、擦除是把数据位重置为1;
3、在写入数据时,只能把数据位上为1的数据改为0;
4、NOR FLASH擦除的最小单位为1个扇区(4KB),读写没有限制,可以单个字节写入读出;
为什么使用SPI
因为UART没有时钟信号,无法控制何时发送数据,也无法保证双发按照完全相同的速度接收数据。因此,双方以不同的速度进行数据接收和发送,就会出现问题。
SPI是一个同步的数据总线,也就是说它是用单独的数据线和一个单独的时钟信号来保证发送端和接收端的完美同步。
时钟是一个振荡信号,它告诉接收端在确切的时机对数据线上的信号进行采样。
产生时钟的一侧称为主机,另一侧称为从机。总是只有一个主机(一般来说可以是微控制器/MCU),但是可以有多个从机(后面详细介绍);
数据的采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低)。具体要看对SPI的配置;
整体的传输大概可以分为以下几个过程:
主机先将
NSS
信号拉低,这样保证开始接收数据;当接收端检测到时钟的边沿信号时,它将立即读取数据线上的信号,这样就得到了一位数据(1
bit
); 由于时钟是随数据一起发送的,因此指定数据的传输速度并不重要,尽管设备将具有可以运行的最高速度(稍后我们将讨论选择合适的时钟边沿和速度)。主机发送到从机时:主机产生相应的时钟信号,然后数据一位一位地将从
MOSI
信号线上进行发送到从机;主机接收从机数据:如果从机需要将数据发送回主机,则主机将继续生成预定数量的时钟信号,并且从机会将数据通过
MISO
信号线发送;
具体如下图所示;
注意,SPI是“全双工”(具有单独的发送和接收线路),因此可以在同一时间发送和接收数据,另外SPI的接收硬件可以是一个简单的移位寄存器。这比异步串行通信所需的完整UART要简单得多,并且更加便宜;
SPI模式
STM32上对NSS引脚的管理提供了软件管理和硬件管理两种方式,可以通过SPI_CR1寄存器中的SSM位设置这两种方式:
STM32 SPI NSS大揭秘_stm32的软件模式下nss不能像普通io输出高电平_andylauren的博客-CSDN博客
时钟频率
SPI总线上的主机必须在通信开始时候配置并生成相应的时钟信号。在每个SPI时钟周期内,都会发生全双工数据传输。
主机在MOSI
线上发送一位数据,从机读取它,而从机在MISO
线上发送一位数据,主机读取它。
就算只进行单向的数据传输,也要保持这样的顺序。这就意味着无论接收任何数据,必须实际发送一些东西!在这种情况下,我们称其为虚拟数据;
从理论上讲,只要实际可行,时钟速率就可以是您想要的任何速率,当然这个速率受限于每个系统能提供多大的系统时钟频率,以及最大的SPI传输速率。
时钟极性 CKP/Clock Polarity
除了配置串行时钟速率(频率)外,SPI主设备还需要配置时钟极性。
根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据;
CKP可以配置为1或0。这意味着您可以根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。您必须参考设备的数据手册才能正确设置CKP和CKE。
CKP = 0
:时钟空闲IDLE
为低电平0
;
CKP = 1
:时钟空闲IDLE
为高电平1
;
时钟相位 CKE /Clock Phase (Edge)
除配置串行时钟速率和极性外,SPI主设备还应配置时钟相位(或边沿)。根据硬件制造商的不同,时钟相位通常写为CKE或CPHA;
顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;
-
CKE = 0
:在时钟信号SCK
的第一个跳变沿采样; -
CKE = 1
:在时钟信号SCK
的第二个跳变沿采样;
时钟配置总结
综上几种情况,下图总结了所有时钟配置组合,并突出显示了实际采样数据的时刻;
其中黑色线为采样数据的时刻; 蓝色线为SCK时钟信号;
具体如下图所示;
模式编号
SPI的时钟极性和相位的配置通常称为 SPI模式,所有可能的模式都遵循以下约定;具体如下表所示;
工作模式 | 时钟极性CPOL | 时钟相位CPHA | |
---|---|---|---|
SP0 | 0 | 0 | 上升沿采样,下降沿发送,空闲状态为低电平
|
SP1 | 0 | 1 | 下降沿采样,上升沿发送,空闲状态为低电平
|
SP2 | 1 | 0 | 下降沿采样,上升沿发送,空闲状态为高电平
|
SP3 | 1 | 1 | 上升沿采样,下降沿发送,空闲状态为高电平
|
优缺点
SPI通讯的优势
使SPI作为串行通信接口脱颖而出的原因很多;
全双工串行通信;
高速数据传输速率。
简单的软件配置;
极其灵活的数据传输,不限于8位,它可以是任意大小的字;
非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。
SPI的缺点
没有硬件从机应答信号(主机可能在不知情的情况下无处发送);
通常仅支持一个主设备;
需要更多的引脚(与I2C不同);
没有定义硬件级别的错误检查协议;
与RS-232和CAN总线相比,只能支持非常短的距离;
SPI 初始化结构体详解
编程要点
(1) 初始化通讯使用的目标引脚及端口时钟;
(2) 使能 SPI 外设的时钟;
(3) 配置 SPI 外设的模式、地址、速率等参数并使能 SPI 外设;
(4) 编写基本 SPI 按字节收发的函数;
(5) 编写对 FLASH 擦除及读写操作的的函数;
(6) 编写测试程序,对读写数据进行校验。
- PB_12 作为SPI2_NSS引脚,需要选择模式,是 主机模式 还是从机模式,在此处为主机模式,配置为软件管理,在作为软件管理时,PB12为一个输出GPIO口。(可以参考上面SPI模式)
- PB_13 作为SPI2_SCK时钟引脚,由主机产生发送给从机,所以配置为输出,又因为是作为SPI的时钟线,是13引脚的复用功能,所以配置为复用推挽输出(可以参考上面SPI特性中的SCK)
- PB_14 作为SPI2_MISO引脚,主机输入,从机输出(数据来自从机),所以配置为浮空输入(可以参考上面SPI特性中的MISO)
- PB_15 作为SPI2_MOSI引脚,主机输出,从机输入(数据来自主机),所以配置为输出,又因为是作为SPI的MOSI线,是15引脚的复用功能,所以配置为复用推挽输出(可以参考上面SPI特性中的MOSI)
引脚代码编写
//时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,ENABLE);
GPIO_InitTypeDef GPIOStruct={0};
//CS --PB12 --通用推挽输出
GPIOStruct.GPIO_Pin = GPIO_Pin_12;
GPIOStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIOStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB,&GPIOStruct);
GPIO_SetBits(GPIOB,GPIO_Pin_12); //不要片选 置高
//SCLK --PB13 --SPI2_SCK --复用推挽输出
//MOSI --PB15 --SPI2_MOSI --复用推挽输出
GPIOStruct.GPIO_Pin = GPIO_Pin_13|GPIO_Pin_15;
GPIOStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOB,&GPIOStruct);
//MISO --PB14 --浮空输入
GPIOStruct.GPIO_Pin = GPIO_Pin_14;
GPIOStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIOStruct);
FreeRTOS操作系统
FreeRTOS操作系统优点
高可移植性,代码主要 C 语言编写。
高效的软件定时器。
强大的跟踪执行功能。
堆栈溢出检测功能。
任务数量不限。
任务优先级不限。
任务优先级数值越大,优先级越高 中断优先级的数值越小,优先级越高
stm32优先级分为抢占式优先级与子优先级,抢占式优先级可以打断优先级低的任务,
当抢占式优先级相同时,子优先级越小,优先执行。
系统移植
1.原码代码
找到FreeRTOS
代码,具体路径在FreeRTOSv10.2.1\FreeRTOS\Source
在工程文件中创建FreeRTOS文件夹
在里面创建source文件放原码
将原码复制到工程文件夹中的FreeRTOS文件夹中的source文件中。
2.头文件
将include头文件整体复制到工程文件夹中的FreeRTOS文件夹中。
3.接口文件
下面在工程文件夹中的FreeRTOS文件夹中新建文件夹protable.
将这两个文件复制到工程文件夹中的FreeRTOS文件夹中的protable文件夹中。
4.内存管理
将这些文件全选复制到工程文件夹中的FreeRTOS文件夹中的新建的文件夹menmmang文件夹中
(其实在这里实际上用的是heap_4算法的内存管理)
5.功能裁剪文件
将此文件复制到任意文件夹中即可,这里为了方便放在工程文件中的FreeRTOS文件夹的include文件夹中。
以上已经将FreeRTOS操作系统需要的文件已经全部整理完毕下一步将这些文件添加到工程中!
对于FreeRTOSConfig.h这个功能裁剪文件添加到user文件夹
中,方便管理功能!!!(它其实不用添加,因为是.h文件。这里添加是为了方便管理)
在工程中新增组FreeRTOS/src
FreeRTOS/port
FreeRTOS/men
,将对应组中文件添加到工程中。
包函头文件路径include
,和portable
。只需要包涵这两个文件就行
接下来操作宏
#include "stm32f10x.h"
/* 我们可以添加的宏定义 */
#define configUSE_TIME_SLICING 1 //使能时间片调度(默认式使能的)
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 //硬件计算前导零指令,如果所使用的, MCU 没有这些硬件指令的话此宏应该设置为 0
#define configUSE_TICKLESS_IDLE 0 //置 1:使能低功耗 tickless 模式;置 0:保持系统节(tick)中断一直运行
#define configUSE_QUEUE_SETS 1 //启用队列
#define configUSE_TASK_NOTIFICATIONS 1 //开启任务通知功能,默认开启
#define configUSE_MUTEXES 1 //使用互斥信号量
#define configUSE_RECURSIVE_MUTEXES 1 //使用递归互斥信号量
#define configUSE_COUNTING_SEMAPHORES 1 //为 1 时使用计数信号量
#define configQUEUE_REGISTRY_SIZE 10 //设置可以注册的信号量和消息队列个数
#define configUSE_APPLICATION_TASK_TAG 0
#define configSUPPORT_DYNAMIC_ALLOCATION 1 //支持动态内存申请
#define configUSE_MALLOC_FAILED_HOOK 0 //使用内存申请失败钩子函数
#define configCHECK_FOR_STACK_OVERFLOW 1// 大于 0 时启用堆栈溢出检测功能,如果使用此功能用户必须提供一个栈溢出钩子函数如果使用的话此值可以为 1 或者 2,因为有两种栈溢出检测方法
#define configGENERATE_RUN_TIME_STATS 0 //启用运行时间统计功能
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
#define configUSE_TIMERS 1 //启用软件定时器
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES-1) //软件定时器优先级
#define configTIMER_QUEUE_LENGTH 10 //软件定时器队列长度
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE*2) //软件定时器任务堆栈大小
//可选函数配置选项
#define INCLUDE_xTaskGetSchedulerState 1
#define INCLUDE_eTaskGetState 1
#define INCLUDE_xTimerPendFunctionCall 1
//中断服务函数 也可以修改起始文件
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
//---------------------------------------------------------------------
//一下内容可以不进行复制
/*源文件自带宏定义 可以修改*/
#define configUSE_PREEMPTION 1 //置 1: RTOS 使用抢占式调度器;置 0:
RTOS 使用协作式调度器(时间片)
#define configUSE_IDLE_HOOK 0 //置 1:使用空闲钩子(Idle Hook 类似于回调函数);置 0:忽略空闲钩子
#define configUSE_TICK_HOOK 0 //置 1:使用时间片钩子(Tick Hook);置 0:忽略时间片钩子
#define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) // 写入实际的CPU 内核时钟频率,也就是 CPU 指令执行频率
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) //RTOS系统节拍中断的频率。即一秒中断的次数,每次中断 RTOS 都会进行任务调度
#define configMAX_PRIORITIES ( 5 ) //可使用的最大优先级
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) //空闲任务使用的堆栈大小
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //系统所有总的堆大小
#define configMAX_TASK_NAME_LEN ( 16 ) //任务名字字符串长度
#define configUSE_TRACE_FACILITY 0 //启用可视化跟踪调试
#define configUSE_16_BIT_TICKS 0 //系统节拍计数器变量数据类型,1表示为16 位无符号整形,0表示为 32 位无符号整形
#define configIDLE_SHOULD_YIELD 1 //空闲任务放弃 CPU 使用权给其他同优先级的用户任务
/* Co-routine definitions. */
#define configUSE_CO_ROUTINES 0 //启用协程,启用协程以后必须添加文件croutine.c
#define configMAX_CO_ROUTINE_PRIORITIES (2) //协程的有效优先级数目
/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */
#define INCLUDE_vTaskPrioritySet 1
#define INCLUDE_uxTaskPriorityGet 1
#define INCLUDE_vTaskDelete 1
#define INCLUDE_vTaskCleanUpResources 1
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_vTaskDelayUntil 1
#define INCLUDE_vTaskDelay 1
/* This is the raw value as per the Cortex-M3 NVIC. Values can be 255
(lowest) to 0 (1?) (highest). */
#define configKERNEL_INTERRUPT_PRIORITY 255
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 /* equivalent to 0xb0, or priority
11. */
/* This is the value being used as per the ST library which permits 16
priority values, 0 to 15. This must correspond to the
configKERNEL_INTERRUPT_PRIORITY setting. Here 15 corresponds to the lowest
NVIC value of 255. */
#define configLIBRARY_KERNEL_INTERRUPT_PRIORITY 15 //中断最低优先级
将上面的宏定义复制到工程文件中的FreeRTOSConfig.h中
例如:
然后编译后报重复定义,是因为操作系统上的中断服务函数与裸机上的中断服务函数冲突了。
解决方法:将裸机上的中断函数注释掉。
接着编译
报vApplicationStackOverflowHook函数未定义。
解决方法:复制这个函数名,Ctrl+f直接搜
方法1.`#if( configCHECK_FOR_STACK_OVERFLOW > 0 )`跳转到这个宏。
可以进行置零关闭。
方法2.编写vApplicationStackOverflowHook函数(在main文件中编写(包涵task.h头文件))
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName )
{
while(1)
{
printf("task stack over:%s\r\n",pcTaskName);
}
}
//这个函数的功能是检测堆栈的溢出情况,当执行到此函数时,说明堆栈溢出,打印具体是哪个任务出现的问题。
以上,操作系统变已经配置完成,接下来便是操作系统的使用
使用文档
开发者文档:(FreeRTOS Features - FreeRTOS)
API 引用:(FreeRTOS API categories)
任务之间的4种运行状态
任务创建
FreeRTOS是实时操作系统,优先级高的先执行,优先级低的需要等优先级高的执行完毕或者阻塞才能让优先级低的工作。
编写任务入口函数
void Task(voide *param)
所有的任务都是无限死循环,使用while(1),或者for(;;)
例子:
//任务入口函数
void LedTask(void *param)
{
for(;;)
{
LED1Toggle;
vTaskDelay(500);
}
}
任务创建(xTaskCreate函数)
TaskHandle_t ledhand=NULL; //任务句柄 (重要)
int main(void)
{
BaseType_t xreturn; //任务创建的返回值
xTaskCreate( LedTask, //任务入口函数
LEDTASKNAME, //任务名称
256, //任务堆栈的 字数
NULL, //入口函数的参数
2, //任务的优先级
&ledhand //任务句柄
);
if(xreturn==pdPASS)
{
printf("ledtask create success\r\n");
}
}
任务删除
参数为要删除任务的任务句柄,当为NULL时删除自己
vTaskDelete(NULL);//参数为要删除任务的任务句柄,当为NULL时删除自己
(第7.2讲 FreeRTOS任务创建和删除实验(动态方法)_哔哩哔哩_bilibili)
临界区
FreeRTOS是实时操作系统,优先级高的先执行,优先级低的需要等优先级高的执行完毕或者阻塞才能让优先级低的工作。
当创建不同优先级任务时,会发生优先级翻转,可以加临界值!
用于保护关键代码段,确保在执行期间不被中断打断,防止优先级反转
但临界区并不是不会被打断,系统调度与外部中断能够打断临界区
注意:一定要记得退出临界区!!!
taskENTER_CRITICAL(); //进入基本临界区
taskEXIT_CRITICAL(); //退出基本临界区
适用场合:
外设:需严格按照时序初始化的外设:SPI IIC等。
任务的挂起与恢复
挂起与恢复 需要的宏
INCLUDE_vTaskSuspend
置为1
中断恢复需要的宏 xTaskResumeFromISR
与 INCLUDE_vTaskSuspend
同时置为1
挂起:相当于暂停,可恢复。
-
vTaskSuspend()
挂起
用于挂起任务。当调用此函数时,当前任务将被挂起,即任务的状态从"Running"变为"Suspended"。被挂起的任务将不再执行,直到它被显式地恢复。在任务被挂起期间,它不会被调度器调度执行。
函数原型为:
void vTaskSuspend(TaskHandle_t xTaskToSuspend);
参数 xTaskToSuspend
是要挂起的任务的句柄。通过传入任务的句柄,可以指定要挂起的特定任务。当参数为NULL时挂起自己。
-
vTaskResume()
恢复挂起用于恢复被挂起的任务。当调用此函数并传入被挂起任务的句柄时,相应的任务将从挂起状态恢复到就绪状态,允许调度器重新将其调度执行。
函数原型为:
void vTaskResume(TaskHandle_t xTaskToResume);
参数
xTaskToResume
是要恢复的任务的句柄。通过传入任务的句柄,可以指定要恢复的特定任务无论挂起多少次,只需要调用一次恢复就能恢复挂起。
-
xTaskResumeFromISR()
中断中恢复在中断处理程序中恢复被挂起的任务时,应使用与中断上下文兼容的任务恢复函数。FreeRTOS提供了
xTaskResumeFromISR()
函数来在中断上下文中恢复挂起的任务。 (带FromISR后缀的都是中断函数中专用的)函数原型为:
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume);
参数
xTaskToResume
是要恢复的任务的句柄。通过传入任务的句柄,可以指定要恢复的特定任务。xTaskResumeFromISR()
函数应在中断服务例程(ISR)中调用,以确保任务恢复操作在中断上下文中执行。由于中断上下文具有特殊的要求和限制,因此必须使用与中断上下文兼容的函数进行任务操作。需要注意以下几点:
-
xTaskResumeFromISR()
函数返回一个BaseType_t
类型的值,要对返回值进行判断。如果需要进行任务切换,返回值为 pdTRUE,否则为 pdFALSE。 -
由于在中断上下文中执行任务恢复操作,任务不会立即开始执行。它们将在调度器决定在何时恢复中断处理程序之后再次执行。
-
仅在使用合适的调度器开关函数(例如
taskYIELD_FROM_ISR()
)的情况下,才会发生任务切换。这将确保恢复的任务具有正确的优先级,并在需要时进行调度。
-
信号量
二值信号量
二值信号量的本质: 是一个队列长度为 1的队列,该队列就只有空和满两种情况,这就是二值。
适用范围:用于互斥访问或任务同步,与互斥信号量比较类似,但是二值信号量有可能会导致优先级翻转的问题 ,所以二值信号量更适合用于同步!
二值信号量API函数
使用二值信号量,必须先释放才能在获取
-
创建二值信号量:
SemaphoreHandle_t xSemaphoreCreateBinary(void);
返回值:
是指向该信号量的句柄(`SemaphoreHandle_t`)。如果二值信号量创建失败,返回的句柄将为 NULL。因此,需要在创建后检查句柄是否为 NULL,以确保成功创建了二值信号量。
示例用法:
// 创建二值信号量
SemaphoreHandle_t binarySemaphore = xSemaphoreCreateBinary();
if (binarySemaphore != NULL) {
// 二值信号量创建成功
} else {
// 二值信号量创建失败
}
注意:
创建二值信号量的函数返回一个指向信号量的句柄。该句柄可以用于后续对信号量的操作,如释放信号和获取信号。
记得在使用完信号量后,最终要使用
vSemaphoreDelete()
函数删除信号量以释放其相关资源。
vSemaphoreDelete(binarySemaphore);//删除信号
释放信号:
在FreeRTOS中,释放信号量使用xSemaphoreGive()
函数。
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
参数 xSemaphore
是一个指向信号量的句柄。该函数会将信号量的计数值加1,表示释放了一个资源。如果有其他任务正在等待获取该信号量,它们中的一个将被唤醒以继续执行。
返回值:
是BaseType_t
类型的值,如果成功释放信号量,则返回值为 pdPASS
。这表示成功地释放了一个资源,并且可能唤醒了一个等待该信号量的任务。
获取信号:
获取信号量有两种函数可以选择:xSemaphoreTake()
和xSemaphoreTakeFromISR()
。
-
xSemaphoreTake()
函数用于在任务中获取信号量。BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
参数
xSemaphore
是一个指向信号量的句柄。xTicksToWait
是等待获取信号量的超时时间。如果信号量的计数值大于0,则将其减1,表示成功获取了一个资源。如果信号量的计数值为0,则任务将被阻塞等待,直到有其他任务释放信号量或超时。xSemaphoreTakeFromISR()
函数用于在中断服务例程(ISR)中获取信号量。BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
参数
xSemaphore
是一个指向信号量的句柄。pxHigherPriorityTaskWoken
是一个指向BaseType_t
类型的变量,用于指示是否有更高优先级的任务需要立即执行。该函数通常在中断服务例程中使用,可以从中断上下文中获取信号量。
使用二值信号量的过程:
创建二值信号量---> 释放二值信号量 -----> 获取二值信号量
优先级翻转
高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。从现象上看,就像是中优先级的任务比高优先级任务具有更高的优先权 (即优先级翻转)
互斥信号
互斥信号量使用流程
创建互斥信号量 -----> 获取信号量 ----->释放信号量
注意:互斥信号量与二值,计数信号量不同,在创建互斥信号量时便系统自动释放一次信号量。
创建互斥信号量
xSemaphoreCreateMutex()
函数创建互斥信号量(Mutex Semaphore)。互斥信号量用于控制对共享资源的访问,确保同一时间只有一个任务能够访问共享资源。
示例代码:
SemaphoreHandle_t mutex; // 互斥信号量的句柄
void createMutexSemaphore() {
mutex = xSemaphoreCreateMutex(); // 创建互斥信号量
if (mutex != NULL) {
// 互斥信号量创建成功
} else {
// 互斥信号量创建失败
}
}
在上述示例中,xSemaphoreCreateMutex()
函数用于创建互斥信号量,并将返回的句柄存储在 mutex
变量中。创建成功时,mutex
变量将包含互斥信号量的句柄;创建失败时,mutex
变量将为 NULL
。
互斥信号量句柄:用于后续对互斥信号量的操作,例如获取和释放。
调用
xSemaphoreTake()
函数获取互斥信号量调用
xSemaphoreGive()
函数释放互斥信号量。
优先级继承:
当一个互斥信号量正在被一个低优先级的任务持有时,如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级。
二值信号量和互斥量的区别
二值信号量通常用于同步(任务与任务或任务与中断的同步),二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空的
互斥信号量适合用于简单的互斥访问,需要互斥访问的应用中。当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。
二值信号量用于多任务间实现同步。本质上就是一个任务等待另一个任务完成某项操作后再继续执行。
互斥信号量用于保护共享资源,确保同时只有一个任务可以访问共享资源,起到多任务访问共享资源的互斥。
● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
计数型信号量
同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关心队列是否为空即可。计数型信号量通常用于如下两个场合:
事件计数:在这个场合中,每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值),其他任务会获取信号量
资源管理:在这个场合中,信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为 0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量,比如停车场一共有 100 个停车位,那么创建信号量的时候信号量值就应该初始化为 100。
LVGL界面
移植lvgl
1. 下载LVGL源码
https://github.com/lvgl/lvgl/tree/v7.11.0 7.11版本
src文件(源文件) 端口文件:examples----->porting文件
1.创建一个名为Lvgl的文件夹,将lvgl/src目录复制到Lvgl目录下
2.将lvgl /examples / porting文件夹复制到Lvgl目录下
3.将lvgl/lvgl.h文件以及lvgl/lv_conf_template.h文件复制到Lvgl目录下
如图:
4.将lv_conf_template.h文件更名为lv_conf.h
5.修改port目录下所需要的文件名字,我们只使用了屏幕的显示功能,因此我们只修改显示接口的文件名字,将lv_port_disp_template.c/.h更名为lv_port_disp.c/.h
6.在Lvgl目录下再创建一个app目录,用于存放我们后期自己写的应用层界面代码
7.打开工程,在工程目录下新建三个分组,分别为Lvgl/app、Lvgl/porting、Lvgl/src三个目录
8.添加文件到工程目录中,porting目录下只添加lv_port_disp.c,以及Lvgl目录下的lv_conf.h文件
9.在src目录下,添加Lvgl/src目录下除去gpu文件夹外的所有文件夹内的c文件。
10.配置头文件路径,把Lvgl文件夹下所有包含h文件的路径,在工程属性中进行配置
2. 修改配置文件
1、打开lv_port_disp.c/.h文件,修改如下内容:
2、修改lv_conf.h文件如下图所示,修改后编译代码,这个时候代码就没有错误了。
3、接下来适配屏幕接口到lvgl上,先修改lv_conf.h内的宏定义,通过它可以设置库的基本行为,裁剪不需要模块和功能,在编译时调整内存缓冲区的大小等等,我们先修改一些必须修改的定义,后期的功能我们在具体项目中再做裁剪。
4、继续适配屏幕接口到lvgl上、修改lv_port_disp.c文件中的显示接口函数,用于适配我们的屏幕与lvgl,包含lcd屏幕显示的头文件。
5、修改屏幕显示初始化函数lv_port_disp_init,我们用方法一显示,同时修改屏幕的大小。
6、修改disp_init函数,该函数一般将我们的屏幕初始化放进去,也可以在硬件层初始化屏幕,这里就可以不写底层屏幕的初始化。
7、修改disp_flush函数,该函数是lvgl绘制界面的关键函数,是用于绘制界面的最基本的函数,也是lvgl与底层屏幕的绘制适配接口函数。