STM32寄存器和hal库开发学习,及一些注意事项
本文最后更新于7 天前,其中的信息可能已经过时,如有错误请发送邮件到3449421627@qq.com

外部中断

外部中断的寄存器配置:

#include "key.h"
void Key_Init(void){

    //打开gpiof和afio的时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPFEN;
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    //配置输入模式,上下拉
    GPIOF->CRH &= ~GPIO_CRH_MODE10;
    GPIOF->CRH |= GPIO_CRH_CNF10_1; 
    GPIOF->CRH &= ~GPIO_CRH_CNF10_0;
    //给下拉,初始值为低电平
    GPIOF->ODR &= ~GPIO_ODR_ODR10;
    //配置afio的引脚复用,告诉来中断的是具体哪个GPIOx
    AFIO->EXTICR[2] |= AFIO_EXTICR3_EXTI10_PF;
    //配置exti,的触发方式和是否中断屏蔽
    EXTI->RTSR |= EXTI_RTSR_TR10;
    EXTI->IMR |= EXTI_IMR_MR10;
    //NVIC,这里用的库函数,自己手动配置寄存器也可以,就是有点复杂,直接调库
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(EXTI15_10_IRQn,15);
    NVIC_EnableIRQ(EXTI15_10_IRQn);
}
//中断服务程序
void EXTI15_10_IRQHandler(void){
    //先清除中断挂起标志位
    EXTI->PR |= EXTI_PR_PR10;
    //消抖
    Delay_ms(10);
    if ((GPIOF->IDR &= GPIO_IDR_IDR10) != RESET){
        LED_Toggle(LED1);
    }
}

hal库

void EXTI15_10_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI15_10_IRQn 0 */

  /* USER CODE END EXTI15_10_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(KEY1_Pin);
  /* USER CODE BEGIN EXTI15_10_IRQn 1 */

  /* USER CODE END EXTI15_10_IRQn 1 */
}

/* USER CODE BEGIN 1 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
  //是否是exti10传来的中断请求
  if(GPIO_Pin == KEY1_Pin){
    //清除中断挂起位,hal已经在底层默认清除了
  HAL_Delay(10);
  if (HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_SET)
  {
    HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
    
  }
   
  }
}
/* USER CODE END 1 */

上面的代码是在hal库里面的,在EXTI15_10_IRQHandler中有个

HAL_GPIO_EXTI_IRQHandler里面有个回调函数,并且自动清除了挂起寄存器,回调函数就是可以重自定义,并且他能接收一个引脚,这样就可以单独写出来,判断引脚到底是哪个,然后进行执行中断代码。

串口

在串口中我们单片机是usart,在电脑上的串口一般是RS232和RS485,其实底层就是usart只是,增加了电气标准,rs485更是发送信号变为了两根线,差分电路,抗干扰能力变得更强了,但是变为半双工了,如果分别两根线发和收,一共就四根线了,就是RS422了

波特率的产生

25.3.4

假设我们要一个115200的波特率

72000000/(115200*6)=39.0625
39的16进制位0x27(前12位)
0.0625*16 = 1(后四位小数部分)
image-20250323123212286

轮询的方式

寄存器实现:

#include "usart.h"
void USART_Init(void){
    //配置时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;//打开gpio
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;//打开串口1
    
    //gpio工作模式
    //pa9:复用推挽输出,CNF-10,MODE-11;
    //PA10:浮空输入,CNF-01,MODE-00;
    GPIOA->CRH |=GPIO_CRH_MODE9;
    GPIOA->CRH |=GPIO_CRH_CNF9_1;
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;

    GPIOA->CRH &=~GPIO_CRH_MODE10;
    GPIOA->CRH &=~GPIO_CRH_CNF10_1;
    GPIOA->CRH |= GPIO_CRH_CNF10_0; 

    //串口配置
    //波特率设置
    USART1->BRR = 0x271;
    //使能
    USART1->CR1 |= (USART_CR1_UE | USART_CR1_TE | USART_CR1_RE);
    //其他配置
    USART1->CR1 &=~USART_CR1_M;
    USART1->CR1 &=~USART_CR1_PCE;
    USART1->CR2 &=~USART_CR2_STOP;
}
void USART_SendChar(uint8_t ch){
    //判断sr里的txe是否为1
    while ((USART1->SR & USART_SR_TXE) == 0)
    {}
    //向DR写入新的要发送的数据
    USART1->DR = ch;

    
}
uint8_t USART_ReceiveChar(void){
    while ((USART1->SR & USART_SR_RXNE) == 0)
    {}
    return USART1->DR;
    
}

发送接收字符串

void USART_SendString(uint8_t *str , uint8_t size){
    for (uint8_t i = 0; i < size; i++)
    {
        USART_SendChar(str[i]);
    }
    
}

void USART_ReceiveString(uint8_t buffer[],uint8_t *size){
    uint8_t i =0;
    while (1)
    {
        
        while ((USART1->SR & USART_SR_RXNE) == 0)
        {
            if ((USART1->SR & USART_SR_IDLE))
            {
                *size = i;
                return;
            }
            
        }
        buffer[i] = USART1->DR;
        i++;
    }
    
}

hal库

  uint8_t buffer[100] = {0};
  while (1)
  {
    if (HAL_UART_ == HAL_OK)
    {
      HAL_UART_Transmit(&huart1,buffer,10,1000);
    }

定长字符串发送

变长字符串的发送

HAL_UARTEx_ReceiveToIdle

一直接收到空间帧为止

  uint8_t buffer[100] = {0};
  uint16_t size =0;
  while (1)
  {
    if (HAL_UARTEx_ReceiveToIdle(&huart1,buffer,100,&size,HAL_MAX_DELAY) == HAL_OK)
    {
      HAL_UART_Transmit(&huart1,buffer,size,HAL_MAX_DELAY);
    }
 }

printf重定向

打印在串口

printf底层主要是fputc这个,更改一下就可以重定向

去keil里面选择,这个一定要勾选,才能使用stdio.h

image-20250324133152317
\\重写fputs函数,usart.c
int fputc(int ch,FILE * file){
    USART_SendChar(ch);
    return ch;
}
\\main.c
	printf("c=%d\n",c);
	printf("hello,world\t\n");

这样就实现了寄存器的打印方法。

hal库print重定向

//usart.h同样要引入stdio.h
    int fputc(int ch,FILE * file){
      HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,1000);
      return ch;
    }

小tick

既要收又要发的时候,GPIO必须配置为复用输出模式(推挽或者开漏),比如I2C

I2C协议

协议上是

高位先行,跟usart不一样,usart是低位先行

image-20250324151306422

开始了后,第一步先传送设备的地址,但是在最后一位还要有方向

软件模拟IC2协议,只有寄存器能做

清除缓冲区的函数

meset(buffer,0,sizeof(buffer));

实现很麻烦,还是用硬件吧,解放cpu

\\i2c.c
#include "i2c.h"

// 初始化
void I2C_Init(void)
{
    // 1. 配置时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;

    // 2. GPIO工作模式配置:复用开漏输出 CNF-11,MODE-11
    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11 |
                   GPIO_CRH_CNF10 | GPIO_CRH_CNF11);

    // 3. I2C2配置
    // 3.1 硬件工作模式
    I2C2->CR1 &= ~I2C_CR1_SMBUS;
    I2C2->CCR &= ~I2C_CCR_FS;

    // 3.2 选择输入的时钟频率
    I2C2->CR2 |= 36;

    // 3.3 配置CCR,对应数据传输速率100kb/s,SCL高电平时间为 5us
    I2C2->CCR |= 180;

    // 3.4 配置TRISE,SCL上升沿最大时钟周期数 + 1
    I2C2->TRISE |= 37;

    // 3.5 使能I2C2模块
    I2C2->CR1 |= I2C_CR1_PE;
}

// 发出起始信号
uint8_t I2C_Start(void)
{
    // 产生一个起始信号
    I2C2->CR1 |= I2C_CR1_START;

    // 引入一个超时时间
    uint16_t timeout = 0xffff;

    // 等待起始信号发出
    while ((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout)
    {
        timeout--;
    }
    return timeout ? OK : FAIL;
}

// 设置接收完成之后发出停止信号
void I2C_Stop(void)
{
    I2C2->CR1 |= I2C_CR1_STOP;
}

// 主机设置使能应答信号
void I2C_Ack(void)
{
    I2C2->CR1 |= I2C_CR1_ACK;
}

// 主机设置使能非应答信号
void I2C_Nack(void)
{
    I2C2->CR1 &= ~I2C_CR1_ACK;
}

// 主机发送设备地址,并等待应答
uint8_t I2C_SendAddr(uint8_t addr)
{
    // 直接将要发送的地址给到DR
    I2C2->DR = addr;

    // 等待应答
    uint16_t timeout = 0xffff;
    while ((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout)
    {
        timeout--;
    }
    // 访问SR2,清除ADDR标志位
    if (timeout > 0)
    {
        I2C2->SR2;
    }

    return timeout ? OK : FAIL;
}

// 主机发送一个字节的数据(写入),并等待应答
uint8_t I2C_SendByte(uint8_t byte)
{
    // 1. 先等待DR为空,上一个字节数据已经发送完毕
    uint16_t timeout = 0xffff;
    while ((I2C2->SR1 & I2C_SR1_TXE) == 0 && timeout)
    {
        timeout--;
    }

    // 2. 将要发送的字节放入DR中
    I2C2->DR = byte;

    // 3. 等待应答
    timeout = 0xffff;
    while ((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout)
    {
        timeout--;
    }
    return timeout ? OK : FAIL;
}

// 主机从EEPROM接收一个字节的数据(读取)
uint8_t I2C_ReadByte(void)
{
    // 1. 先等待DR为满
    uint16_t timeout = 0xffff;
    while ((I2C2->SR1 & I2C_SR1_RXNE) == 0 && timeout)
    {
        timeout--;
    }

    // 2. 将收到的字节数据返回
    return timeout ? I2C2->DR : FAIL;
}

m24c02.c

/*
 * @Author: wushengran
 * @Date: 2024-05-31 11:48:48
 * @Description:
 *
 * Copyright (c) 2024 by atguigu, All Rights Reserved.
 */
#include "m24c02.h"

// 初始化
void M24C02_Init(void)
{
    I2C_Init();
}

// 向EEPROM写入一个字节
void M24C02_WriteByte(uint8_t innerAddr, uint8_t byte)
{
    // 1. 发出开始信号
    I2C_Start();

    // 2. 发送写地址
    I2C_SendAddr(W_ADDR);

    // 3. 发送内部地址
    I2C_SendByte(innerAddr);

    // 4. 发送具体数据
    I2C_SendByte(byte);

    // 5. 发出一个停止信号
    I2C_Stop();

    // 延迟等待写入周期结束
    Delay_ms(5);
}

// 读取EEPROM的一个字节
uint8_t M24C02_ReadByte(uint8_t innerAddr)
{
    // 1. 发出开始信号
    I2C_Start();

    // 2. 发送写地址(假写)
    I2C_SendAddr(W_ADDR);

    // 3. 发送内部地址
    I2C_SendByte(innerAddr);

    // 4. 发出开始信号
    I2C_Start();

    // 5. 发送读地址(真读)
    I2C_SendAddr(R_ADDR);

    // 6. 设置非应答
    I2C_Nack();

    // 7. 设置在接收下一个字节后发出停止信号
    I2C_Stop();

    // 8. 读取一个字节
    uint8_t byte = I2C_ReadByte();

    return byte;
}

// 连续写入多个字节(页写)
void M24C02_WriteBytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size)
{
    // 1. 发出开始信号
    I2C_Start();

    // 2. 发送写地址
    I2C_SendAddr(W_ADDR);

    // 3. 发送内部地址
    I2C_SendByte(innerAddr);

    // 利用循环不停发送数据
    for (uint8_t i = 0; i < size; i++)
    {
        // 4. 发送具体数据
        I2C_SendByte(bytes[i]);
    }

    // 5. 发出一个停止信号
    I2C_Stop();

    // 延迟等待写入周期结束
    Delay_ms(5);
}

// 连续读取多个字节
void M24C02_ReadBytes(uint8_t innerAddr, uint8_t *buffer, uint8_t size)
{
    // 1. 发出开始信号
    I2C_Start();

    // 2. 发送写地址(假写)
    I2C_SendAddr(W_ADDR);

    // 3. 发送内部地址
    I2C_SendByte(innerAddr);

    // 4. 发出开始信号
    I2C_Start();

    // 5. 发送读地址(真读)
    I2C_SendAddr(R_ADDR);

    // 利用循环连续读取多个字节
    for (uint8_t i = 0; i < size; i++)
    {
        // 6. 设置应答或非应答
        if (i < size - 1)
        {
            I2C_Ack();
        }
        else
        {
            I2C_Nack();

            // 7. 设置发出停止信号
            I2C_Stop();
        }
        // 8. 读取一个字节
        buffer[i] = I2C_ReadByte();
    }
}

hal库

/*
 * @Author: wushengran
 * @Date: 2024-05-31 11:48:48
 * @Description:
 *
 * Copyright (c) 2024 by atguigu, All Rights Reserved.
 */
#include "m24c02.h"

// 初始化
void M24C02_Init(void)
{
    MX_I2C2_Init();
}

// 向EEPROM写入一个字节
void M24C02_WriteByte(uint8_t innerAddr, uint8_t byte)
{
    HAL_I2C_Mem_Write(&hi2c2,W_ADDR,innerAddr,I2C_MEMADD_SIZE_8BIT,&byte,1,1000);

    HAL_Delay(5);
}

// 读取EEPROM的一个字节
uint8_t M24C02_ReadByte(uint8_t innerAddr)
{
    uint8_t byte;
    HAL_I2C_Mem_Read(&hi2c2,R_ADDR,innerAddr,I2C_MEMADD_SIZE_8BIT,&byte,1,1000);

    return byte;
}

// 连续写入多个字节(页写)
void M24C02_WriteBytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size)
{
    HAL_I2C_Mem_Write(&hi2c2,W_ADDR,innerAddr,I2C_MEMADD_SIZE_8BIT,bytes,size,1000);
    // 延迟等待写入周期结束
    HAL_Delay(5);
}

// 连续读取多个字节
void M24C02_ReadBytes(uint8_t innerAddr, uint8_t *buffer, uint8_t size)
{
    HAL_I2C_Mem_Read(&hi2c2,R_ADDR,innerAddr,I2C_MEMADD_SIZE_8BIT,buffer,size,1000);
}

定时器

systick定时器

理论上要配置nvic但是,systick是内核的,这就不用配置优先级了。

void SysTick_init(void){
	SysTick->LOAD = 7999999;

	SysTick->CTRL &= ~SysTick_CTRL_CLKSOURCE;

	SysTick->CTRL |= SysTick_CTRL_TICKINT;

	SysTick	->CTRL |= SysTick_CTRL_ENABLE;

}
void SysTick_Handler(void){
	if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG)
	{
		LED_Toggle(LED1);
	}
	
}

这里默认的就是用的72M的时钟源 SysTick->CTRL &= ~SysTick_CTRL_CLKSOURCE置0就是8M,1就是72M

在hal库中底层有一个全局变量 uwTick是以毫秒为单位计时。但是这个变量跟其他的一些系统参数有关,不能改变他的大小,这个时候获取一秒就可以在中断函数中

if (uwIick %1000 == 0){
    led_toggle();
}

systick定时器只能向下计数,只有24位。

基本定时器

只能向上计数,只有16位。,没有外部IO,只能记时。

初始化配置

#include "timer6 .h"

void Timer_Init(void){
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;

    TIM6->PSC = 7199;
    TIM6->ARR = 9999;
//  更新中断使能
    TIM6->DIER |= TIM_DIER_UIE;
//配置中断
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(TIM6_IRQn,5);
    NVIC_EnableIRQ(TIM6_IRQn);
//  开启定时器
    TIM6->CR1 |= TIM_CR1_CEN;
}
void TIM6_IRQHandler(void){
    TIM6->SR &= ~TIM_SR_UIF;//清除标志位,这个得软件清除,不能硬件清除
    LED_Toggle(LED1);
    
}

这样就可以实现每秒翻转一下led灯了

使用hal库就是在中断的时候,选择回调函数,

选择PeriodElapsedCallback();

然后直接翻转引脚

void TIM6_IRQHandler(void)
{

  HAL_TIM_IRQHandler(&htim6);
}


void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
  if (htim->Instance == TIM6)
  {
  HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
  }
  
}

但是这样发现没有效果,这里有个坑

配置完了定时器,但是没有打开定时器,默认是不打开的,所以要使能。

image-20250326210517215

这里用了中断就选图上那个。

现在就可以了

在做练习的时候(systick和tim6同时控制两个灯同时亮灭的时候)

有个坑

就是他们会不同步,每次都是tim6先进入中断

在底层是因为,定时器默认初始的时候,会产生一个刷新事件,会导致一次中断,所以他会。

有个思路就是引入一个变量,表示进入中断的次数,只要第一次进入中断的时候不翻转电平就可以了。

但是这种方法也有点low,最好的方法就是 底层主要是因为一开始他的ARR和psc刷不进去,所以以1/72M的速度进了一次中断,翻转了一次,所以也根本看不出来,既然是刷不技巧怒,那就直接用一个寄存器,可以手动刷进去,产生一个刷新事件,那就是

TIMx_EGR中的UG标志位

通用寄存器

基本定时器的功能全都可以,而且除了内部时钟源,还能选择外部时钟源。

从外部输入的引脚脉冲来作为时钟源,

外部时钟源模式1的通道1 的TI1FP1,和通道2的TI2FP2,还能做输入捕获

外部时钟源模式2 的TITMx_ETR直接就是直接外部输入的引脚脉冲来作为时钟源

PWM(脉冲宽度调制

三个重要参数

  • 周期
  • 频率
  • 占空比:高电平宽度t除以周期T(占空比才会影响亮度,一般不改变周期和频率,这样才能改变等效的电压值)

输出比较功能

一般用来控制输出方波的、

大多用于输出pwm波

也可以输出其他波形

但是只能是方波

寄存器

pwm模式1

image-20250327112908978

pwm模式2

image-20250327113108335

以后常用的就是pwm1的模式,小于的时候是高电平。

配置:

#include "timer5.h"

void TIM5_Init(void)
{  
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;
    //2.
    GPIOA->CRL |= GPIO_CRL_MODE1;
    GPIOA->CRL |= GPIO_CRL_CNF1_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF1_0;

    //3.定时器配置
    TIM5->PSC = 7199;
    TIM5->ARR = 99;

    TIM5->CR1 &= ~TIM_CR1_DIR;//默认是向上计数,这步不要也行

    //设置通道2的CCR2的值
    TIM5->CCR2 =50;

    //配置通道2为输出模式
    TIM5->CCMR1 &= ~TIM_CCMR1_CC2S;
    //配置通道2pwm1模式
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_2;
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_1;
    TIM5->CCMR1 &= ~TIM_CCMR1_OC2M_0;

    //开启使能输出通道
    TIM5->CCER |= TIM_CCER_CC2E;


}

void TIM5_Start(void){
    TIM5->CR1 |= TIM_CR1_CEN;
}

void TIM5_Stop(void){
    TIM5->CR1 &= ~TIM_CR1_CEN;
}

void Set_DutyCycle(uint8_t dutycycle){
    TIM5->CCR2 = dutycycle;
}

主函数实现

/*
 * @Author: wushengran
 * @Date: 2024-05-23 15:14:48
 * @Description:
 *
 * Copyright (c) 2024 by atguigu, All Rights Reserved.
 */
#include "usart.h"
#include "m24c02.h"
#include <string.h>
#include "delay.h"
#include "timer5.h"
uint32_t count = 0;
int main(void)
{
	TIM5_Init();
	USART_Init();
	printf("hello,world\n");
	TIM5_Start();
	uint8_t dutycycle = 0;
	uint8_t dir = 0;
	// 无限循环
	while (1)
	{
		if (dir == 0)
		{
			/* 占空比增大*/
			dutycycle +=1;
			if (dutycycle >=99)
			{
				dir = 1;
			}
		}
		else		
		{
			/* 占空比增大*/
			dutycycle -=1;
			if (dutycycle <= 1)
			{
				dir = 0;
			}
		}
		
		//设置占空比
		Set_DutyCycle(dutycycle);
		Delay_ms(10);
		
	}
}
hal库
void SetDuty(uint8_t duty){
__HAL_TIM_SetCompare(&htim5,TIM_CHANNEL_2,duty);//这个函数设置占空比的
}

然后在主函数里面,需要开启pwm

  HAL_TIM_PWM_Start(&htim5,TIM_CHANNEL_2);

LCD液晶屏背光源

在PB0上

测量PWM的频率和周期(输入捕获)

需要让定时器的时钟要大一点,然后LOAD也要大一点,直接取65535最大,防止测不出来

hal库默认选择的TIM4的CH1通道使用的引脚不对修改方法

直接选中他默认的引脚,在芯片图中,点击reset,然后选择要使用的引脚tim4复用,然后再重新配置。

hal库中断回调函数

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){
  if(htim->Instance == TIM4){
    __HAL_TIM_SetCounter(&htim4,0);//计数器清零
  }
}

输入捕获一种特殊的模式——PWM输入模式

这种模式不仅可以测量周期,还可以测量占空比。

从模式控制定时器复位。

第一通道TI1FP1高电平触发,同时触发从模式,TI1FP2低电平触发。

高级定时器

相比于通用定时器多了

  • 重复计数器
  • 互补输出(只要ch1,ch2,ch3有,ch4没有)image-20250329144015946
  • 死区生成

生成死区时间,延迟一段时间再开通。

image-20250329144317168
  • 短路输入信号

DMA模块

ME2ME(特殊)(ROM到RAM)

寄存器

DMA1.c文件

#include"dma.h"
//初始化
void DMA1_Init(void){
    //开启时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    //数据传输方向
    DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM;
    DMA1_Channel1->CCR |= DMA_CCR1_DIR;

    //设置传输的数据宽度,默认是8位
    DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE;
    DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE;

    //开启地址自增
    DMA1_Channel1->CCR |= DMA_CCR1_PINC;
    DMA1_Channel1->CCR |= DMA_CCR1_MINC;

    //开启数据传输完成中断标志
    DMA1_Channel1->CCR |= DMA_CCR1_TCIE;

    //开启中断服务nvic配置
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(DMA1_Channel1_IRQn,4);
    NVIC_EnableIRQ(DMA1_Channel1_IRQn);

}
//数据传输
void DMA1_Transmit(uint32_t srcAddr,uint32_t destAddr,uint16_t datalen){
    //1.设置外设地址
    DMA1_Channel1->CPAR |= destAddr;
    //2.设置存储器地址
    DMA1_Channel1->CMAR |= srcAddr;
    //3.设置传输的数据量
    DMA1_Channel1->CNDTR = datalen;
    //4.开启通道,开始传输数据
    DMA1_Channel1->CCR |= DMA_CCR1_EN;
}

//中断服务程序
void DMA1_Channel1_IRQHandler(void){
    //判断一下中断标志位
    if (DMA1->ISR & DMA_ISR_TCIF1)
    {
        //清除中断标志位
        DMA1->IFCR |= DMA_IFCR_CTCIF1;
        //关闭DMA通道
        DMA1_Channel1->CCR &= ~DMA_CCR1_EN;
        DMA1_flag = 1;
    }
}

main.c文件

/*
 * @Author: wushengran
 * @Date: 2024-05-23 15:14:48
 * @Description: 
 * 
 * Copyright (c) 2024 by atguigu, All Rights Reserved. 
 */
#include "led.h"
#include "delay.h"
#include "usart.h"
#include "string.h"
#include "stdio.h"
#include "dma.h"
uint8_t buff[100]={0};
uint8_t size = 0;
uint8_t DMA1_flag = 0;
//定义全局常量放在rom中作为数据源
const uint8_t src[] = {10,20,30,40};
//定义数据变量用来存储接收到的数据
uint8_t dest[4] = {0};

int main(void)
{
	USART_Init();
	DMA1_Init();
	//打印变量常量地址,看看看在哪里
	printf("src = %p, dest = %p ",src,dest);
	//开启 dma通道进行传输
	DMA1_Transmit((uint32_t)src,(uint32_t)dest,4);

	while(1)
	{
			if (DMA1_flag)
			{
				for (uint8_t i = 0; i < 4; i++)
				{
					printf("%d\t",dest[i]);
				}
				DMA1_flag = 0;
				
			}
			
		}
}

hal库

it.c

void DMA1_Channel1_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel1_IRQn 0 */

  /* USER CODE END DMA1_Channel1_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_memtomem_dma1_channel1);
  /* USER CODE BEGIN DMA1_Channel1_IRQn 1 */
  HAL_DMA_Abort_IT(&hdma_memtomem_dma1_channel1);//关闭DMA通道
  flag = 1;
  /* USER CODE END DMA1_Channel1_IRQn 1 */
}

main.c

printf("hello,world\n");
  printf("src=%p\tdest=%p\n",src,dest);
  //开启DMA传输
  HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1,(uint32_t)src,(uint32_t)dest,4);
  while (1)
  {
    if (flag)
    {
      for (uint8_t  i = 0; i < 4; i++)
      {
        printf("%d\n",dest[i]);
      }
      flag = 0;   
    }
  }

RAM到外设(串口)

寄存器

dma.c

/*
 * @Author: wushengran
 * @Date: 2024-06-07 11:35:48
 * @Description: 
 * 
 * Copyright (c) 2024 by atguigu, All Rights Reserved. 
 */
#include "dma.h"

// 初始化
void DMA1_Init(void)
{
    // 1. 开启时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    // 2. DMA相关配置
    // 2.1 数据传输方向: 从存储器读,发往串口外设
    DMA1_Channel4->CCR |= DMA_CCR4_DIR;

    // 2.2 数据宽度: 8位 - 00
    DMA1_Channel4->CCR &= ~DMA_CCR4_PSIZE;
    DMA1_Channel4->CCR &= ~DMA_CCR4_MSIZE;

    // 2.3 地址自增:开启自增,串口地址不能自增
    DMA1_Channel4->CCR &= ~DMA_CCR4_PINC;
    DMA1_Channel4->CCR |= DMA_CCR4_MINC;

    // 2.4 开启数据传输完成中断标志
    DMA1_Channel4->CCR |= DMA_CCR4_TCIE;

    // 2.5 使能串口的DMA传输功能
    USART1->CR3 |= USART_CR3_DMAT;

    // 2.6 开启循环传输功能
    DMA1_Channel4->CCR |= DMA_CCR4_CIRC;

    // 3. NVIC配置
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(DMA1_Channel4_IRQn, 2);
    NVIC_EnableIRQ(DMA1_Channel4_IRQn);

}

// 数据传输
void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen)
{
    // 1. 设置外设地址
    DMA1_Channel4->CPAR = destAddr;

    // 2. 设置存储器地址
    DMA1_Channel4->CMAR = srcAddr;

    // 3. 设置传输的数据量
    DMA1_Channel4->CNDTR = dataLen;

    // 4. 开启通道,开始传输数据
    DMA1_Channel4->CCR |= DMA_CCR4_EN;
}

// 中断服务程序
void DMA1_Channel4_IRQHandler(void)
{
    // 判断中断标志位
    if (DMA1->ISR & DMA_ISR_TCIF4)
    {
        // 清除中断标志位
        DMA1->IFCR |= DMA_IFCR_CTCIF4;

        // 关闭DMA通道
        // DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
    }
    
}

main.c

/*
 * @Author: wushengran
 * @Date: 2024-05-23 15:14:48
 * @Description: 
 * 
 * Copyright (c) 2024 by atguigu, All Rights Reserved. 
 */
#include "usart.h"
#include "dma.h"
#include "delay.h"

// 定义变量数组,放置在RAM中,用来存储接收到的数据
uint8_t src[4] = {'a','b','c','d'};

int main(void)
{
	// 初始化
	USART_Init();
	DMA1_Init();

	printf("Hello, world!\n\n");
	Delay_ms(1);

	// 开启DMA通道进行传输
	DMA1_Transmit((uint32_t)src, (uint32_t)&(USART1->DR), 4);

	while(1)
	{}
}

hal库

  uint8_t str[] = {'a','b','c','d','e'};
  HAL_UART_Transmit_DMA(&huart1,str,5);

配置好后,两行代码搞定。

ADC

寄存器

adc.c

#include "adc.h"

// 初始化
void ADC1_Init(void)
{
    // 1. 时钟配置
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    // CFGR:ADCPRE - 10,6分频,得到 12MHz
    RCC->CFGR |= RCC_CFGR_ADCPRE_1;
    RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;

    // 2. GPIO工作模式,PC0,模拟输入,00 00
    GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);

    // 3. ADC配置
    // 3.1 工作模式:禁用扫描模式
    ADC1->CR1 &= ~ADC_CR1_SCAN;

    // 3.2 启用连续转换模式(单曲循环)、
    ADC1->CR2 |= ADC_CR2_CONT;

    // 3.3 数据右对齐(默认)
    ADC1->CR2 &= ~ADC_CR2_ALIGN;

    // 3.4 设置通道10的采样时间,001 - 7.5个时钟周期
    ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
    ADC1->SMPR1 |= ADC_SMPR1_SMP10_0;

    // 3.5 规则组通道序列配置
    // 3.5.1 规则组中的通道个数 L
    ADC1->SQR1 &= ~ADC_SQR1_L;

    // 3.5.2 将通道号 10 保存到序列中的第一位
    ADC1->SQR3 &= ~ADC_SQR3_SQ1;
    ADC1->SQR3 |= 10 << 0;

    // 3.5 选择软件触发AD转换
    // ADC1->CR2 |= ADC_CR2_EXTTRIG;
    // ADC1->CR2 |= ADC_CR2_EXTSEL;    // 选择的就是SWSTART控制AD转换
}

// 开启转换
void ADC1_StartConvert(void)
{
    // 1. 上电唤醒
    ADC1->CR2 |= ADC_CR2_ADON;

    // 2. 执行校准
    ADC1->CR2 |= ADC_CR2_CAL;
    // 等待校准完成
    while (ADC1->CR2 & ADC_CR2_CAL)
    {}

    // 3. 启动转换
    // ADC1->CR2 |= ADC_CR2_SWSTART;
    ADC1->CR2 |= ADC_CR2_ADON;

    // 4. 等待转换完成
    while ((ADC1->SR & ADC_SR_EOC) == 0)
    {}
}

// 返回转换后的模拟电压值
double ADC1_ReadV(void)
{
    return ADC1->DR * 3.3 / 4095;
}

main.c

#include "usart.h"
#include "adc.h"
#include "delay.h"

int main(void)
{
	// 初始化
	USART_Init();
	ADC1_Init();

	printf("Hello, world!\n");

	// 开启AD转换
	ADC1_StartConvert();

	while(1)
	{
		// 向串口发送打印转换结果
		printf("V = %.2f\n", ADC1_ReadV());
		Delay_ms(1000);
	}
}

hal库

  printf("hello,world\n");
  //开启校准
  HAL_ADCEx_Calibration_Start(&hadc1);
  //启动ADC
  HAL_ADC_Start(&hadc1);

  while (1)
  {
    // 获取转换结果,进行计算
    double v = HAL_ADC_GetValue(&hadc1)*3.3/4095;
    printf("%.2f",v);
    HAL_Delay(500);
    
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

tips:

在GPIO中输入模式可以不用配置时钟,因为底层的电路就是通的

要把获取结果放到while循环中并且打印,不能把获取结果放在while外面,然后再轮询打印。

校准需要时间的。

SPI

SP1_W25Q32芯片(NOR FLASH)

软件模拟

w25q32.c

/*
 * @Author: wushengran
 * @Date: 2024-06-12 15:54:23
 * @Description:
 *
 * Copyright (c) 2024 by atguigu, All Rights Reserved.
 */
#include "w25q32.h"

// 初始化
void W25Q32_Init(void)
{
    SPI_Init();
}

// 读取ID
void W25Q32_ReadID(uint8_t *mid, uint16_t *did)
{
    SPI_Start();

    // 1. 发送指令 9fh
    SPI_SwapByte(0x9f);

    // 2. 获取制造商ID(为了读取数据,发送什么不重要)
    *mid = SPI_SwapByte(0xff);

    // 3. 获取设备ID
    *did = 0;
    *did |= SPI_SwapByte(0xff) << 8;
    *did |= SPI_SwapByte(0xff) & 0xff;

    SPI_Stop();
}

// 开启写使能
void W25Q32_WriteEnable(void)
{
    SPI_Start();
    SPI_SwapByte(0x06);
    SPI_Stop();
}

// 关闭写使能
void W25Q32_WriteDisable(void)
{
    SPI_Start();
    SPI_SwapByte(0x04);
    SPI_Stop();
}

// 等待状态不为忙(busy)
void W25Q32_WaitNotBusy(void)
{
    SPI_Start();

    // 发送读取状态寄存器指令
    SPI_SwapByte(0x05);

    // 等待收到的数据末位变成0
    while (SPI_SwapByte(0xff) & 0x01)
    {
    }

    SPI_Stop();
}

// 擦除段(sector erase),地址只需要块号和段号
void W25Q32_EraseSector(uint8_t block, uint8_t sector)
{
    // 首先等待状态不为忙
    W25Q32_WaitNotBusy();

    // 开启写使能
    W25Q32_WriteEnable();

    // 计算要发送的地址(段首地址)
    uint32_t addr = (block << 16) + (sector << 12);

    SPI_Start();

    // 发送指令
    SPI_SwapByte(0x20);

    SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
    SPI_SwapByte(addr >> 8 & 0xff);  // 第二个字节
    SPI_SwapByte(addr >> 0 & 0xff);  // 第三个字节

    SPI_Stop();

    W25Q32_WriteDisable();
}

// 写入(页写)
void W25Q32_PageWrite(uint8_t block, uint8_t sector, uint8_t page, uint8_t *data, uint16_t len)
{
    // 首先等待状态不为忙
    W25Q32_WaitNotBusy();

    // 开启写使能
    W25Q32_WriteEnable();

    // 计算要发送的地址(页首地址)
    uint32_t addr = (block << 16) + (sector << 12) + (page << 8);

    SPI_Start();

    // 发送指令
    SPI_SwapByte(0x02);

    // 发送24位地址
    SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
    SPI_SwapByte(addr >> 8 & 0xff);  // 第二个字节
    SPI_SwapByte(addr >> 0 & 0xff);  // 第三个字节

    //  依次发送数据
    for (uint16_t i = 0; i < len; i++)
    {
        SPI_SwapByte(data[i]);
    }

    SPI_Stop();

    W25Q32_WriteDisable();
}

// 读取
void W25Q32_Read(uint8_t block, uint8_t sector, uint8_t page, uint8_t innerAddr, uint8_t *buffer, uint16_t len)
{
    // 首先等待状态不为忙
    W25Q32_WaitNotBusy();

    // 计算要发送的地址
    uint32_t addr = (block << 16) + (sector << 12) + (page << 8) + innerAddr;

    SPI_Start();

    // 发送指令
    SPI_SwapByte(0x03);

    // 发送24位地址
    SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
    SPI_SwapByte(addr >> 8 & 0xff);  // 第二个字节
    SPI_SwapByte(addr >> 0 & 0xff);  // 第三个字节

    //  依次读取数据
    for (uint16_t i = 0; i < len; i++)
    {
        buffer[i] = SPI_SwapByte(0xff);
    }

    SPI_Stop();
}

spi.c

#include "spi.h"

// 初始化
void SPI_Init(void)
{
    // 1. 开启时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    // 2. GPIO工作模式
    // PA5、PA7、PC13:通用推挽输出,CNF = 00,MODE = 11
    GPIOC->CRH |= GPIO_CRH_MODE13;
    GPIOC->CRH &= ~GPIO_CRH_CNF13;

    GPIOA->CRL |= GPIO_CRL_MODE5;
    GPIOA->CRL &= ~GPIO_CRL_CNF5;

    GPIOA->CRL |= GPIO_CRL_MODE7;
    GPIOA->CRL &= ~GPIO_CRL_CNF7;

    // PA6:MISO,浮空输入,CNF = 01,MODE = 00
    GPIOA->CRL &= ~GPIO_CRL_MODE6;
    GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
    GPIOA->CRL |= GPIO_CRL_CNF6_0;

    // 3. 选择SPI的工作模式 0:SCK空闲0
    SCK_LOW;

    // 4. 片选不使能
    CS_HIGH;

    // 5. 延时
    SPI_DELAY;
}

// 数据传输的开始和结束
void SPI_Start(void)
{
    CS_LOW;
}
void SPI_Stop(void)
{
    CS_HIGH;
}

// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte)
{
    // 定义变量保存接收到的字节
    uint8_t rByte = 0x00;

    // 用一个循环,依次交换8位数据
    for (uint8_t i = 0; i < 8; i++)
    {
        // 1. 先准备要发送的数据(最高位),送到MOSI
        if (byte & 0x80)
        {
            MOSI_HIGH;
        }
        else
        {
            MOSI_LOW;
        }

        // 左移一位
        byte <<= 1;

        // 2. 拉高时钟信号,形成一个上升沿
        SCK_HIGH;
        SPI_DELAY;

        // 3. 在MISO上采样Flash传来的数据
        // 先做左移
        rByte <<= 1;

        if (MISO_READ)
        {
            rByte |= 0x01;
        }

        // 4. 拉低时钟,为下次数据传输做准备
        SCK_LOW;
        SPI_DELAY;
    }

    return rByte;
}

main.c

#include "usart.h"
#include "w25q32.h"
#include <string.h>
int main(void)
{
	// 1. 初始化
	USART_Init();
	W25Q32_Init();

	printf("尚硅谷SPI软件模拟实验开始...\n");

	// 2. 读取ID进行测试
	uint8_t mid = 0;
	uint16_t did = 0;
	W25Q32_ReadID(&mid, &did);
	printf("mid = %#x, did = %#x\n", mid, did);

	// 3. 段擦除
	W25Q32_EraseSector(0, 0);

	// 4. 页写
	W25Q32_PageWrite(0, 0, 0, "12345678", 8);

	// 5. 读取
	uint8_t buffer[10] = {0};
	W25Q32_Read(0, 0, 0, 2, buffer, 6);

	printf("buffer = %s\n", buffer);

	while (1)
	{
	}
}

FSMC(灵活的静态存储器控制器)

在我们扩展的sram里面想写入数据进行测试,怎么生成的变量指定放到扩展区域呢

可以使用关键字attribute

uint8_t v1 __attribute__ ((at(0x68000000)));
v1 = 10 ;
//必须是全局变量,只能单独声明,不能同时赋值,给的地址必须是4的整数,内存对齐
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇