carry
发布于 2024-05-06 / 38 阅读
0
0

使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容博客作业5 中断与UART

使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容博客作业5 中断与UART

零、 创新点

《嵌入式技术与基础 第六版》原书教材随书附赠了一块stm32L431的开发板,配套内容都是以此为基础打造的,甚至作者还专门为此开发了一套ide,但是:

  • 现在stm32主流的教程生态用的都是stm32f103c8t6这款芯片,原书的教程虽然能很好的为你解释嵌入式开发的原理,但是要进一步学习,你还是不得不寻找其他教程,购买主流的开发板
  • 作者的ide完全是出于本书学习设计的,对于实际开发毫无意义,若将来想做点嵌入式,还需要重新学习一套开发栈
  • 全新套书卖100。作为抠门带血生,肯定要考虑二手的,但是二手书通常不会带有原书的附件。单买开发板,某宝60,某鱼甚至没有。起码截至我使用该书学习的时间(2024年3月)是这样的,后期使用此书的前辈多了,估计二手市场会变好。
  • 最重要的是,我已经有一个stm32f103c8t6的开发板了

综上,无论是从学习角度还是省钱角度,使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容都是非常合适的

这套博客是广州大学计算机学院嵌入式系统课程的作业,我用stm32f103c8t6开荒,也是为了后来的同学做贡献

一、 从一个简单的串口实验说起

①在电脑的输出窗口显示下一个字符,如收到A显示B;

②亮灯:收到字符G,亮绿灯;收到字符R,亮红灯;收到字符B,亮蓝灯;收到其他字符,不亮灯。

实现方式:

以下是使用hal库实现功能的核心代码:

/* USER CODE BEGIN 2 */
  char receive_char[1];
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_UART_Receive(&huart2, (uint8_t*)&receive_char, 1,HAL_MAX_DELAY);
	  switch(receive_char[0]){
	  case 'R':
		  HAL_GPIO_WritePin(RED_GPIO_Port, RED_Pin, GPIO_PIN_SET);
		  break;
	  case 'G':
		  HAL_GPIO_WritePin(GREEN_GPIO_Port, GREEN_Pin, GPIO_PIN_SET);
		  break;
	  case 'Y':
		  HAL_GPIO_WritePin(YELLO_GPIO_Port, YELLO_Pin, GPIO_PIN_SET);
		  break;
	  default:
		  receive_char[0] = receive_char[0] + 1;
		  HAL_UART_Transmit(&huart2,(uint8_t*)receive_char, 1,HAL_MAX_DELAY);
		  HAL_GPIO_WritePin(YELLO_GPIO_Port, YELLO_Pin, GPIO_PIN_RESET);
		  HAL_GPIO_WritePin(RED_GPIO_Port, RED_Pin, GPIO_PIN_RESET);
		  HAL_GPIO_WritePin(GREEN_GPIO_Port, GREEN_Pin, GPIO_PIN_RESET);

	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

串口操作都浓缩到这两行代码里

HAL_UART_Receive(huart, pData, Size, Timeout);//接收
HAL_UART_Transmit(huart, pData, Size, Timeout);//发送

甚至在原书的金葫芦ide中,代码已经做了printf重定向,直接使用printf就能打印,和普通的c语言编程没什么区别

printf("这是金葫芦ide");

不过,从底层来看,可没那么简单,而且串口通信能做的远超这个

我们可以使用中断实现这个代码逻辑:当uart收到信息时,触发中断,运行相应代码

在实际应用中,为了提高效率和实时性,通常会利用中断机制来管理发送和接收过程。当发生发送完成、接收准备好或错误事件时,相应的中断标志会被置位,通过中断服务程序来响应这些事件,执行相应的数据处理或错误恢复操作。

二、 UART的底层原理

在软件层面,实现STM32 UART通信主要涉及以下几个关键步骤和原理:

1. 初始化UART外设

在使用UART之前,首先需要通过编程配置其相关寄存器来初始化UART外设:

  • 选择USART和时钟源:根据项目需求选择合适的USART,并确保其对应的时钟已经使能。

  • 波特率设置:波特率决定了数据传输的速度,计算并设置USART_BRR寄存器以得到所需的波特率。公式通常为

    USARTDIV = f_PCLK / (USART_BAUDRATE * 16)

    其中f_PCLK是USART的输入时钟频率。

  • 数据位、停止位和奇偶校验设置:通过USART_CR1和USART_CR2寄存器配置数据帧的格式,比如8位数据位、1位停止位、无奇偶校验或有奇偶校验等。

  • 使能发送和接收:设置USART_CR1中的TXEIE(发送数据寄存器空中断使能)和RXNEIE(接收数据寄存器非空中断使能),以及TE(发送使能)和RE(接收使能)位。

//相应寄存器地址
volatile uint32_t* rcc = (volatile uint32_t*)0x40021000UL;          //时钟寄存器基地址
volatile uint32_t* rcc_ahb2 = (volatile uint32_t*)((uint32_t)rcc | 0x4CUL); //AHB2总线外设时钟使能寄存器基地址
volatile uint32_t* rcc_apb1 = (volatile uint32_t*)((uint32_t)rcc | 0x58UL); //APB1总线外设时钟使能寄存器基地址
volatile uint32_t* gpioa = (volatile uint32_t*)0x48000000UL;        //gpioa寄存器基地址
volatile uint32_t* gpioa_moder = gpioa;     //gpioa模式寄存器基地址
volatile uint32_t* gpioa_afrl = gpioa + 8;      //gpioa复用功能低位寄存器基地址
volatile uint32_t* usart2 = (volatile uint32_t*)0x40004400UL;       //usart2寄存器基地址
volatile uint32_t* usart2_cr1 = usart2;     //usart2控制寄存器1基地址
volatile uint32_t* usart2_cr2 = usart2 + 1; //usart2控制寄存器2基地址
volatile uint32_t* usart2_cr3 = usart2 + 2; //usart2控制寄存器3基地址
volatile uint32_t* usart2_brr = usart2 + 3; //usart2波特率寄存器基地址
volatile uint32_t* nvic_iser = (volatile uint32_t*)0xE000E100UL;    //nvic中断设置使能寄存器基地址

//用户外设模块初始化
gpio_init(LIGHT_RED,GPIO_OUTPUT,LIGHT_OFF); //初始化红灯
gpio_init(LIGHT_GREEN,GPIO_OUTPUT,LIGHT_OFF);   //初始化绿灯
gpio_init(LIGHT_BLUE,GPIO_OUTPUT,LIGHT_OFF);    //初始化蓝灯

//配置usart2复用
//1、使能gpioa和UART2的时钟
*rcc_ahb2 |= (0x1UL<<0U);       //gpioa时钟使能
*rcc_apb1 |= (0x1UL<<17U);       //UART2时钟使能 

//2、gpioa 端口设置成 usart 复用模式
//2.1 配置gpioa模式寄存器为复用模式(两个引脚)
*gpioa_moder &= ~((0x3UL<<4U)|(0x3UL<<6U));
*gpioa_moder |= ((0x2UL<<4U)|(0x2UL<<6U));
//2.2 配置gpioa复用功能寄存器
*gpioa_afrl &= ~((0xFUL<<8U)|(0xFUL<<12U));
*gpioa_afrl |= ((0x7UL<<8U)|(0x7UL<<12U));

//3、关闭 usart
*usart2_cr1 &= ~(0x1UL);

//4、关闭串口的收发功能
*usart2_cr1 &= ~((0x1UL<<2U)|(0x1UL<<3U));

//5、配置 usart 模式
//5.1 配置数据位长度(8位)
*usart2_cr1 &= ~((0x1UL<<12U)|(0x1UL<<28U));
//5.2 配置过采样模式(16)
*usart2_cr1 &= ~(0x1UL<<15U);
//5.3 配置是否启用校验和校验类型(禁用奇偶校验控制)
*usart2_cr1 &= ~(0x1UL<<10U);
//5.4 配置usart_CR2,将使能位清零。D14—LIN模式使能位、D11—时钟使能位 
*usart2_cr2 &= ~((0x1UL<<14U)|(0x1UL<<11U));
//5.5 配置usart_CR3,将控制寄存器3的三个使能位清零。D5 (SCEN) —smartcard模式使能位、D3 (HDSEL) —半双工选择位、D1 (IREN) —IrDA 模式使能位
*usart2_cr3 &= ~((0x1UL<<5U)|(0x1UL<<3U)|(0x1UL<<1U));

//6、配置波特率因子
uint16_t usartDIV = (uint16_t)((SystemCoreClock/115200));
*usart2_brr = usartDIV;

//7、打开串口的收发功能
*usart2_cr1 |= ((0x1UL<<2U)|(0x1UL<<3U));

//8、打开 usart
*usart2_cr1 |= 0x1UL;

2. 接收数据

接收数据流程主要包括:

  • 等待接收数据:软件监控USART_SR寄存器的RXNE位(接收数据寄存器非空标志)。当RXNE=1,表示有新的数据可供读取。
  • 读取数据:从USART_DR寄存器中读取接收到的数据字节。
  • 处理接收完成:可设置接收中断(RXNEIE),每当接收到新数据时,处理器将被中断唤醒,以便及时处理接收到的信息。
if((*usart2_cr1)&(0x1UL<<5U))   //判断是否使能接收缓冲区非空中断
{
    for (uint32_t i = 0; i < 0xFFFF; ++i)//查询指定次数
    {
        if((*usart2_isr)&(0x1UL<<5U))   //判断读取数据寄存器是否非空
        {
            data = *usart2_rdr;         //读取接收寄存器
            switch(data){
                case 'R':gpio_set(LIGHT_RED,LIGHT_ON);break;
                case 'G':gpio_set(LIGHT_GREEN,LIGHT_ON);break;
                case 'B':gpio_set(LIGHT_BLUE,LIGHT_ON);break;
                default:
                gpio_set(LIGHT_RED,LIGHT_ON);
                gpio_set(LIGHT_BLUE,LIGHT_OFF);
                gpio_set(LIGHT_GREEN,LIGHT_OFF);
                for (uint32_t j = 0; j < 0xFFFF; ++j)//查询指定次数
                    {
                    if((*usart2_isr)&(0x1UL<<7U))   //判断发送数据寄存器是否为空
                    {
                        *usart2_tdr = data + 1;     //回发接收到的内容(内容加一)
                        break;
                    }
                }//end for
            }
            break;
        }
    }
}

三、 中断

1. 中断的基本概念

中断是处理器对外部事件或内部事件的一种响应机制。当一个中断发生时,CPU会暂停当前正在执行的任务,保存其状态,然后跳转到特定的中断服务程序地址去执行相应的处理代码。处理完成后,CPU会恢复之前任务的状态并继续执行。

2. STM32中断体系结构

STM32微控制器基于ARM Cortex-M系列内核,通常使用嵌套向量中断控制器(NVIC)来管理中断。NVIC支持优先级管理,可以实现中断的嵌套,即高优先级中断可以打断低优先级中断的处理。

3. 中断配置与使能

在软件层面,使用STM32需要通过编程来配置和使能中断。这包括以下几个步骤:

  • 中断源配置:根据需要处理的外部或内部事件,配置相应的外设或寄存器,比如GPIO的边沿触发、定时器的溢出等。
  • 中断优先级设置:在NVIC中为每个中断分配优先级。STM32支持多个优先级等级,可以通过编程设置。
  • 中断使能:在NVIC中使能特定中断,允许它能够打断CPU的正常流程。
//配置usart2中断使能
//1、配置usart2接收缓冲区非空中断使能
*usart2_cr1 |= (0x1UL<<5U);

//2、使能NVIC中USART2的中断
*(nvic_iser + (USART2_IRQn >> 5U)) |= (0x1UL << ((uint32_t)USART2_IRQn & 0x1FUL));

4. 中断向量表

Cortex-M系列处理器使用嵌套向量中断控制器(NVIC),并有一个中断向量表存储在内存的固定位置。这个表列出所有中断的服务程序入口地址。当一个中断发生时,CPU会根据中断号从向量表中取出对应的地址,然后跳转到该地址执行ISR。

g_pfnVectors:

	#略
  .word USART1_IRQHandler
  .word USART2_IRQHandler #我们的中断处理函数
  .word USART3_IRQHandler
	#略
  .word 0
  .word BootRAM        

5. 中断服务程序(ISR)

ISR是用户编写的处理特定中断的函数。为了快速响应,ISR通常要求简短且高效。进入ISR后,CPU会自动保存一些关键寄存器,然后执行ISR中的代码。ISR完成处理后,一般会通过特定指令恢复现场并返回,以便CPU继续执行被中断的任务。

6. 中断的进入与退出

  • 进入中断:中断发生时,CPU保存当前状态(如寄存器值),根据中断向量表跳转到ISR执行。
  • 退出中断:ISR执行完毕后,通过恢复之前保存的CPU状态(如程序计数器PC),CPU回到中断前的执行点继续运行。

四、封装下的实现

介绍完了寄存器编程方式,我们来看点阳间的实现

先在stm32 cube ide中配置引脚,我们以uart2为例

image

启用uart2中断

image

我们让ide生成代码,然后就可以在Core/Src/stm32f1xx_it.c​中看到这个

void USART2_IRQHandler(void)
{
  /* USER CODE BEGIN USART2_IRQn 0 */

  /* USER CODE END USART2_IRQn 0 */
  HAL_UART_IRQHandler(&huart2);
  /* USER CODE BEGIN USART2_IRQn 1 */

  /* USER CODE END USART2_IRQn 1 */
}

系统生成的HAL_UART_IRQHandler(&huart2);​用于帮我们完成最基本的中断处理,比如让中断寄存器重制避免无限中断

别和我一样一股脑得就想在这里完成自己的代码,因为能触发串口中断的有很多,例如校验错误等,不一定是我们要的

HAL库替我们考虑好了,如果是普通的接收中断,HAL_UART_IRQHandler(&huart2);​函数会调用HAL_UART_RxCpltCallback(UART_InitTypeDef *huart)​函数,所以我们只需要重新定义这个函数void HAL_UART_RxCpltCallback(UART_InitTypeDef *huart);​就可以实现自定义收到数据后的中断处理流程

char receive_char[1];//因为有多个函数要访问receive_char,所以定义为全局变量

void HAL_UART_RxCpltCallback(UART_InitTypeDef *huart){
	switch(receive_char[0]){
	case 'R':
		HAL_GPIO_WritePin(RED_GPIO_Port, RED_Pin, GPIO_PIN_SET);
	    break;
    case 'G':
	    HAL_GPIO_WritePin(GREEN_GPIO_Port, GREEN_Pin, GPIO_PIN_SET);
	    break;
    case 'Y':
	    HAL_GPIO_WritePin(YELLO_GPIO_Port, YELLO_Pin, GPIO_PIN_SET);
	    break;
    default:
	    receive_char[0] = receive_char[0] + 1;
	    HAL_UART_Transmit(&huart2,(uint8_t*)receive_char, 1,HAL_MAX_DELAY);
	    HAL_GPIO_WritePin(YELLO_GPIO_Port, YELLO_Pin, GPIO_PIN_RESET);
	    HAL_GPIO_WritePin(RED_GPIO_Port, RED_Pin, GPIO_PIN_RESET);
	    HAL_GPIO_WritePin(GREEN_GPIO_Port, GREEN_Pin, GPIO_PIN_RESET);
 	}
	HAL_UART_Receive_IT(&huart2, (uint8_t*)&receive_char, 1,HAL_MAX_DELAY);//为下次接收开启中断
}

int main(void)
{
	HAL_Init();
	SystemClock_Config();
  	MX_GPIO_Init();
  	MX_USART2_UART_Init();
 	HAL_UART_Receive_IT(&huart2, (uint8_t*)&receive_char, 1,HAL_MAX_DELAY);
 	while (1);
}

输入R、Y后红灯黄灯亮,符合预期效果

关于作者:

欢迎联系!


评论