STM32开发知识点


20220127011217

更新日志

预计下次更新 DMA

2022/1/23

  • 添加了FLASH相关的内容和驱动
  • 添加了SPI的两个驱动,ST7789和W25Q128
    添加了I2C的一个驱动,SGP30

2022/1/21

  • 初始的内容,CubeMX和基本外设,GPIO,串口,ADC,DAC,SPI,I2C

资源下载

Keil: 官网地址
Keil DFP包: 官网地址
STM32CubeMX: 官网地址

Keil开发的基本技巧和函数

  • Target勾选Use MicroLib
  • Output关闭 Browse Information
  • Debug 打开Reset and Run
  • 开发时优化等级调成O(1) 发布时调成O(3)
  • 添加fputc函数,并#include 以将printf重定向到usart

fputc函数

int fputc(int ch, FILE* fp)  // 自定义串口重定向函数,将printf输出到USART2便于调试
{
    HAL_UART_Transmit(&huart2, (uint8_t*)&ch, 1, 0xff);
    return ch;
}

us级计时器函数

us级定时器,需要在CubeMX将TIM的频率调整为1MHz
系统时钟/(预分频+1) 例如 72/(71+1)

define DLY_TIM_Handle (&htim1)

void delay_us(uint16_t nus)
{
HAL_TIM_SET_COUNTER(DLY_TIM_Handle, 0); HAL_TIM_ENABLE(DLY_TIM_Handle);
while (HAL_TIM_GET_COUNTER(DLY_TIM_Handle) < nus)
{ }
HAL_TIM_DISABLE(DLY_TIM_Handle);
}

GPIO

使用前需要先在CubeMX中打开并配置引脚

GPIO参数设置

参数说明

  • GPIO output level - 程序初始化之后该Output口输出的电平信号是高还是低
  • GPIO mode - 输出模式,开漏和推挽,一般选择Push Pull推挽输出即可
  • Pull up/ Pull down 上下拉模式

    上拉电阻的目的是为了保证在无信号输入时,输入端的电平为高电平。而在信号输入为低电平时,输入端的电平也为低电平。如果没有上拉电阻,在没有外界输入的情况下输入端是悬空的,它的电平是未知的无法保证的,上拉电阻就是为了保证无信号输入时输入端的电平为高电平,同样还有下拉电阻它是为了保证无信号输入时输入端的电平为低电平。

  • output speed

    决定IO口驱动电路的响应速度,如果输出速度和配置速度不匹配,会明显看到波形不正常。波形会出现不完整,幅度低等失真现象。
    高速:输出频率高,噪音大,功耗高,电磁干扰强;
    低速:输出频率低,噪音小,功耗低,电磁干扰弱;提高系统EMI(电磁干扰)性能;
    如果IO口的输出速度上M,应该配置成High以上

GPIO输出

将IO口设为低电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
将IO口设为高电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
反转IO口的电平
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);

GPIO读取

读取IO口电平,返回值1为高电平,0为低电平
HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13)

按键中断

CubeMX配置界面

  • 将所需要的pin设置成外部中断模式
  • 在NVIC中打开EXIT line

中断的调用过程,以按键中断为例
按键被按下,MCU直接获取到中断向量表中的按键中断地址
跳转到stm32xxx_it.c中的EXTI15_10_IRQHandler()
20220123015004
在EXTI15_10_IRQHandler()中调用HAL_GPIO_EXTI_IRQHandler(B1_Pin)
注意,此时调用了所有在EXIT[15:10]线上的IRQHandler,例如B1_Pin的Pin_12、Pin_15
实际上中断无法区分是PA15的中断还是PB15的中断,因此在cubemx中只能打开一个pin上的一个口的中断

20220123015041
HAL_GPIO_EXTI_IRQHandler()函数调用了HAL_GPIO_EXTI_Callback()
这就是需要我们编写的中断回调函数
20220123015119
可以看到,这是一个weak函数,我们一般在main.c中编写它的实际逻辑

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) // 按键中断程序
{
    if(GPIO_Pin == GPIO_PIN_15) // 先判断是哪个PIN
    {
        ...
    }
}

串口

在CubeMX中打开串口,再在NVIC中打开串口中断
20220123104712

串口发送数据(阻塞式)
程序会进入当前语句,直到所有数据发送完毕再执行下一条指令,若在规定的时间内没有发送完毕则返回HAL_TIMEOUT
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 0xff);

&huart2是要发送到的串口的结构体指针
&ch是要发送的uint8数组的首地址
1是待发送数组的长度
0xff是超时时间

串口接收数据(阻塞式)
程序会进入当前语句,直到接收到指定长度的数据再执行下一条指令,若在规定的时间内没有接收完毕则返回HAL_TIMEOUT
HAL_UART_Receive(&huart2, (uint8_t *)&ch, 1, 0xff);

&huart2是接收数据的串口的结构体指针
&ch是用于接收的uint8数组的首地址
1是接收长度
0xff是超时时间

串口中断方法

串口发送数据(中断)
用的不多,发送数据一般用的都是阻塞式
程序会在语句中设置好中断,并执行后续的指令,直到数据全部发送完毕触发中断
进入中断的条件是发送完了所有的数据
HAL_UART_Transmit_IT(&huart2, (uint8_t *)&ch, 100);

&huart2是接收数据的串口的结构体指针
&ch是用于接收的uint8数组的首地址
100是发送长度

在main.c中编写串口接收中断

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
   ...
}

串口接收数据(中断)
程序会在语句中设置好中断,并执行后续的指令,直到有数据过来触发中断
进入中断的条件是接收到了指定长度的数据
若数据量没有达到指定的值,即使接收到了数据,还是不会进入中断
HAL_UART_Receive_IT(&huart2, (uint8_t *)&ch, 1);

&huart2是接收数据的串口的结构体指针
&ch是用于接收的uint8数组的首地址
1是接收长度

在main.c中编写串口接收中断

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    HAL_UART_Transmit(&huart2, &ch, 1, 10); // 把收到的字节原样发送出去或存入buffer中
    HAL_UART_Receive_IT(&huart2, &ch, 1);  // 重新注册一次,要不然下次收不到了
}

串口中断接收数据的最佳实践:
用中断方式,一次接收一位,将接收到的存入数组中,规定结束位
在中断中判断是否时结束位,进行后续操作

/*全局变量定义*/
unsigned int Rxlen = 0;
unsigned char Rxbuff;
unsigned char DataBuff[1024];
unsigned RxEnd_FLAG = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
 /* 先判断是哪个UART*/
    if(huart->Instance == USART2)
    {
        Rxlen++; 
        //每接收到一个数据,进入回调数据长度加1
        buffer[Rxlen-1]=Rxbuff; 
        //把每次接收到的数据保存到缓存数组
        if(Rxbuff == 0xff|| Rxlen == 1024)  //接收结束标志位,这个数据可以自定义,根据实际需求,这里只做示例使用,不一定是0xff
        {
            printf("RXLen=%d\r\n",Rxlen);                        
            memset(buffer,0,sizeof(buffer));  // 清空缓存
            Rxlen=0;  // 清空接收长度
            RxEnd_FLAG = 1; // 没有RTT的情况 标记FLAG为1,在main中判断
            // rt_sem_release(&sem_uart); // 有RTT的情况:释放信号量 启动被阻塞的串口线程
        }
        Rxbuff = 0;
        HAL_UART_Receive_IT(&huart2, (uint8_t *)&Rxbuff, 1); // 每接收一个数据,就打开一次串口中断接收,否则只会接收一个数据就停止接收
    }
}


int main()
{
    HAL_UART_Receive_IT(&huart2, (uint8_t *)&Rxbuff, 1); // 在main中打开中断接收
}

ADC模数转换

参考链接: ADC的四种用法:轮询、中断、DMA、定时器触发

在CubeMX中打开并完成基本设置
20220123220435

例如,F410的ADC最大频率是PCLK/4 = 25MHz, 采样周期最短是3 cycles
那么一次采样的时间T = 0.12us 理论最大采样率是8.3M

ADC校准
F1,L4,H7系列均存在,但在F4系列取消了,H7又添加了回来,但不是这个接口
这个接口仅仅适用于F1,L4系列
HAL_ADCEx_Calibration_Start(&hadc1, 100); // ADC校准
开始ADC转换(阻塞)

HAL_ADC_Start(&hadc1);      //启动ADC转换
HAL_ADC_PollForConversion(&hadc1, 10);  //等待转换完成,10为最大等待时间,单位为ms
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC)) 
    ADC_Value = HAL_ADC_GetValue(&hadc1); //获取AD值

开始ADC转换(中断)
HAL_ADC_Start_IT(&hadc1);
ADC中断回调函数

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) //ADC转换完成回调
{
   unsigned short adc_value = HAL_ADC_GetValue(&hadc1);
   HAL_ADC_Start_IT(&hadc1); // 再次打开中断
}

DAC模数转换

DAC启动输出

HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 2048);
HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);

SPI通信

在CubeMX中根据器件的参数配置速率,工作模式,数据长度,注意接线和定义,特别是速率,过快的话会导致通信失败
MISO Master Input Slave Output 主输入从输出
MOSI Master Output Slave input 主输出从输入
注意,主机和从机的口的名字是一样的,不像串口协议
HAL库SPI基本命令

HAL_SPI_Transmit(&hspi1, &TxData, Size, Timeout);  //SPI发送数据
HAL_SPI_Receive(&hspi1, &RxData, Size, Timeout);   //SPI接收数据
HAL_SPI_TransmitReceive(&hspi2,&TxData,&Rxdata,1, 1000); // SPI边发边接受

SPI相关驱动

SPI屏幕驱动ST7789: 下载地址
SPI FLASH驱动W25Q128: 下载地址

I2C通信

参考链接: 博客地址
注意:I2C初始化需要在GPIO前面

HAL_I2C_Master_Transmit(&hi2c2, 0x0B, &TXbuff, size, TIMEOUT); //主机发送 0x0B是设备地址
HAL_I2C_Slave_Receive(&hi2c2, &Rxbuff, size, 100) // 从机接受 从机地址需要和主机发送到的地址一样

I2C相关驱动

SGP30

片上FLASH

if(Flash_If_Write(u8_writebuff, 0x08080000+second*len*2, len*2)== FLASH_OK) // 写入到FLASH中 u8_writebuff是unsigned char数组
    printf("Write Done\n");
else
    printf("Write Error\n");

Flash_If_Read(u8_readbuff, 0x08080000+second*len*2, len*2); // 读取FLASH

自己写的FLASH HAL库驱动: 下载地址


开发过程中遇到的坑(持续更新)

软件方面

L476的DAC的HAL库软件触发无效

需要在下面添加一条SET_BIT(hdac1.Instance->SWTRIGR, DAC_SWTRIGR_SWTRIG1);

DMA初始化需要在器件之前

printf需要勾选use microlib

SGP30需要一段时间等待初始化完成

SPI速率不能太高


文章作者: Allen Hong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Allen Hong !
  目录