Getting started with STM32

实现自行车码表

Posted by Yym on April 17, 2016

利用 Cube 库编写 STM32 裸机程序,实现自行车码表功能。设置 GPIO、UART 和定时器,编写中断响应程序,采用ST-LINK 烧录程序。

建立工程

打开 STM32CubeMX,选择对应的开发板型号,新建工程。

在 Pinout 中设置 USART1 为 Asyncronous。

屏幕快照 2016-04-16 下午1.36.39

Configuration 选项卡中可以设置具体功能。选择 Connnectivity 中的 USART1,设置波特率为 9600 8n1

屏幕快照 2016-04-16 下午1.37.26

选择 Project->Settings,设置 IDE 为 MDK-ARM,选择本地的 Cube Package,然后选择 Generate Code 自动生成工程模版。 屏幕快照 2016-04-16 下午1.41.45

生成的模版工程可以用 Keil 打开,用户在 main.c 中编写代码。

编译&烧录

Keil 编译生成 hex 文件

在 Keil 工程中设置生成 hex 文件,并选择文件保存路径。 屏幕快照 2016-04-16 下午6.03.08

编译成功后,打开 ST-LINK 烧录程序,载入生成的 hex 文件。 屏幕快照 2016-04-16 下午6.07.45

将 STM32 开发板与 ST-LINK 连接,对应引脚如下。

ST-LINK STM32
SWCLK DCLK
SWDIO DIO
3.3V 3.3V
GND GND

连接成功后,点击 Target->Program 进行烧录,注意将 STM32 开发板上的 bt0 置为 1。 之后显示烧录成功,然后将 bt0 复位。屏幕快照 2016-04-16 下午6.17.23

串口输出

USART设置

MX_USART1_UART_Init() 函数中是 CubeMX 自动生成的 USART 配置,初始化 UART。

/* USART1 init function */
void MX_USART1_UART_Init(void)
{

  huart1.Instance = USART1;
  huart1.Init.BaudRate = 9600;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  HAL_UART_Init(&huart1);

}

HAL_UART_Init(&UartHandler) 又调用 stm32f1xx_hal_msp.c 文件中的 HAL_UART_MspInit(UART_HandleTypeDef* huart),这个函数里进行时钟和 GPIO 的初始化设置。

void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{

  GPIO_InitTypeDef GPIO_InitStruct;
  if(huart->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspInit 0 */

  /* USER CODE END USART1_MspInit 0 */
    /* Peripheral clock enable */
    __HAL_RCC_USART1_CLK_ENABLE();

    /**USART1 GPIO Configuration    
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX 
    */
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* USER CODE BEGIN USART1_MspInit 1 */
	__HAL_RCC_USART1_FORCE_RESET(); 
    __HAL_RCC_USART1_RELEASE_RESET(); 
    HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9); 
    HAL_GPIO_DeInit(GPIOA, GPIO_PIN_10);
  /* USER CODE END USART1_MspInit 1 */
  }
}
重定向输出

为了更方便的发送数据,可以重定向 printf 函数,这样可以直接使用 printf 函数来向串口发送数据,这需要我们重写 fputc 函数,这个函数是声明为 weak 的,这就意味着我们的版本会替代系统的版本。这样我们可以在 main 函数中利用 printf 函数直接输出 “Hello” 到串口。

int fputc(int ch, FILE *f) {
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
	return ch;
}				

将 STM32 开发板连接 usb-ttl 接头,对应引脚如下。

USB STM32
3.3V 3.3V
GND GND
TXD A10
RXD A9
PC 端接收

PC 端安装串口驱动程序,USB 连接串口后,显示设备 /dev/tty.SLAB_USBtoUART,设置minicom 程序波特率 9600 8N1 与串口通信。

屏幕快照 2016-04-16 下午6.47.20

轮询

GPIO 配置

配置 PA11 为内部上拉到输入模式,并通过面包板连接一个按钮。将 Speed 设置为 LOW 之后可以减少按钮抖动影响。

  /*Configure GPIO pin : PA11 */
  GPIO_InitStruct.Pin = GPIO_PIN_11;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
主循环检查

通过 HAL_GPIO_ReadPin 读取引脚输入,在 main() 函数中循环检查 PA11 按钮按下,并在按下时在串口输出 “Pressed”,延时去抖动。

  while (1) 
  {
  /* USER CODE END WHILE */
  /* USER CODE BEGIN 3 */
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11) == GPIO_PIN_RESET) {
			HAL_Delay(80);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11) == GPIO_PIN_RESET) 
				printf("Pressed.\n\r");
		}
  /* USER CODE END 3 */

建议自己添加代码时,在 /* USER CODE BEGIN n *//* USER CODE END n */ 之间添加,这样再次通过 CubeMX 修改之后就不会删除添加的代码,否则修改 CubeMX 工程文件重新生成代码会删除添加的代码。

屏幕快照 2016-04-17 上午12.10.25

中断

配置 GPIO 中断

配置 PA12 内部上拉到输入模式,并且下降沿触发中断。

  /*Configure GPIO pin : PA12 */
  GPIO_InitStruct.Pin = GPIO_PIN_12;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

设置中断优先级,enable 中断向量表处理。

  /* EXTI15_10_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
中断标识

程序中设置两个全局变量,一个为计数器,一个为标识。当中断触发时,计数器加1,并设置标识。

当 PA12 出现中断时,会调用 EXTI15_10_IRQHandler() 这个函数进行中断处理,在 stm32f1xx_it.c 中实现它。

这个函数中又要调用 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12); 判断中断标志位,以及调用中断返回函数 HAL_GPIO_EXTI_Callback,然后清除标志位。

/**
* @brief This function handles EXTI line[15:10] interrupts.
*/
void EXTI15_10_IRQHandler(void) {
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
	if (GPIO_Pin == GPIO_PIN_12){
		prsdCnt++;
		prsdFlag = 1;
	}else{
		UNUSED(GPIO_Pin);
	}
}
检查中断标识

在主循环中判断标识,如果标识置位则清除标识并通过串口输出计数值。

	/* PA12 INT */
	if (prsdFlag) {
		prsdFlag = 0;
		printf("Pressed Counter: %d\n", prsdCnt);
	}

屏幕快照 2016-04-16 下午9.25.09

定时器中断

配置定时器

配置 TIM3 向上计数,计数到 199,内部时钟频率为8MHz,Prescaler 分频值范围设置为 8000,这样定时器频率为 200ms

  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 8000;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 199;
  HAL_TIM_Base_Init(&htim3);

设置时钟源为内部时钟

  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig);

设置复位模式,发生触发输入事件时计数器和预分频器能重新初始化

  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig);

设置中断优先级,enable 中断向量表处理。

  /* TIM3_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(TIM3_IRQn);
启动定时器

在 main() 函数中调用启动定时器。

HAL_TIM_Base_Start_IT(&TIM_Handle);
中断处理

与按钮中断类似,每次触发定时器中断将标识置为 1,在主循环检查标识,并输出到串口。

在 stm32f1xx_it.c 里面编写中断处理函数 TIM3_IRQHandler(),这个函数又调用 HAL_TIM_IRQHandler(&htim3);

最终调用回调函数 HAL_TIM_PeriodElapsedCallback() 进行中断处理。

/**
* @brief This function handles TIM3 global interrupt.
*/
void TIM3_IRQHandler(void) {
  HAL_TIM_IRQHandler(&htim3);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
    timFlag = 1;
    timCnt++;
}
串口输出

屏幕快照 2016-04-16 下午9.25.09

码表程序

模式

码表采用两个模式,速度模式和里程模式,通过 PA3 按钮切换。

里程模式显示总里程,速度模式显示当前的近似速度。假设车轮周长为 2 米。

输出

输出采用定时器中断,每 200ms 输出串口一次,并根据当前模式输出对应的速度或里程。

速度

当前速度的计算采用最近 1s 的里程除以时间的近似。因此定时器计数为 5 时计算一次速度。

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  /* USER CODE END WHILE */
  /* USER CODE BEGIN 3 */
		/* PA3 polling */
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == GPIO_PIN_RESET) {
			HAL_Delay(80);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == GPIO_PIN_RESET) {
				printf("Change Mode.\n\r");
				dispMode = 1 - dispMode;
			}
		}
    
		/* PA12 INT */
		if (prsdFlag) {
			prsdFlag = 0;
			//printf("Pressed Counter: %d\n\r", prsdCnt);
		}

		/* TIMER INT */
		if (timFlag) {
			timFlag = 0;
			curDist = prsdCnt * 2.0;
			if (timCnt == 5) {
				speed = (curDist - preDist) * 2.0 / 1.0;
				preDist = curDist; 
				timCnt = 0;
			}	
			if (dispMode)
				sprintf(str, "Distance: %.2lf m \n\r", curDist);
			else 
				sprintf(str, "Speed: %.2lf m/s \n\r", speed);
			printf(str);
		}
  }
  /* USER CODE END 3 */
结果验证

上电后串口输出 Hello STM32,并以 200ms 间隔输出,输出模式为速度或里程。PA3按钮改变速度,PA12按钮改变里程。

屏幕快照 2016-04-17 下午9.54.05

PA3 按钮也可采用中断模式,在 HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) 这个函数中判断引脚并分别处理。

中断驱动

特点
休眠设置

暂停 SysTick,否则它会中断休眠。

HAL_SuspendTick();

然后进入休眠模式,PWR_SLEEPENTRY_WFI 表示可被中断唤醒。

HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);

设置 CPU 中断后重新进入休眠模式。

HAL_PWR_EnableSleepOnExit();
中断处理程序

按钮中断,PA3 模式切换,PA12 旋转计数。

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
	if (GPIO_Pin == GPIO_PIN_12){
		prsdCnt++;
		prsdFlag = 1;
	} 
	else if (GPIO_Pin == GPIO_PIN_3){
		printf("Change Mode.\n\r");
		dispMode = 1 - dispMode;
	}
	else{
		UNUSED(GPIO_Pin);
	}
}
计数器中断
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	curDist = prsdCnt * 2.0;	
	if (timCnt == 5) {
		speed = (curDist - preDist) * 2.0 / 1.0;
		preDist = curDist; 
		timCnt = 0;
	}	
	if (dispMode)
		sprintf(str, "Distance: %.2lf m \n\r", curDist);
	else 
		sprintf(str, "Speed: %.2lf m/s \n\r", speed);
	printf(str);
}

问题

在我建立的工程中,PA11 与 PA12 引脚同时使用时,相互的信号会发生重叠,当把 PA11 引脚改为 PA3 后解决了这个问题。原因可能是 PA12 的中断处理与 PA11 为同一组,所以发生冲突。