最近接触了一些STM32嵌入式开发的工作。本人在此方面完全是门外汉,所以通过博客来记录自己的工作内容,也希望专业人士看到后能够给出一些指点。
该项目是基于STM32系列芯片的自动测温测湿系统,主要作为初学者的学习项目。项目开发采用的是基于HAL库的STM32CubeIDE,初衷是可以快速上手,并且代码方便移植到不同型号的芯片。开发中需要用到的调试软件包括XCOM串口调试助手、网络调试助手。
工作指标 | 工作指标 | 备注 |
---|---|---|
Stm32F103C8T6开发板 | 3.3V供电,主频72MHz | 需要支持至少一个usart接口用于连接WiFi模块。需要虚拟USB接口以输入调试指令和输出调试信息。一个GPIO引脚用于连接温度传感器。 |
ESP8266-01 WiFi模块 | Vcc需要稳定3.3V供电,峰值电流可达500mA,需确保电源模块的稳定性和功率指标。空闲时电流20mA。 | 01型号需要在EN和RST引脚接10k上拉电阻。可用改进型号(ESP8266-01S)替代,则不需要额外接EN和RST引脚。 |
DHT22温度传感器(AM2302) | Vcc 3.3-6V供电,经测试3.3v可正常工作。工作电流1mA。 | 采样周期>2s,每次采样后返回的是上一次采样的数据。Vcc和Gnd切勿接反,否则烧毁。 |
该开发板自带LED,由PA1引脚通过GPIO控制。DHT22使用PA6引脚进行单总线传输。PA1和PA6的GPIO需要单独设置。开启ST-Link调试需要在SYS->Debug
中选择Serial wire
,CubeMx将自动设置PA13、PA14引脚。
其他用到的引脚:DHT22所需的自定义微秒级延时将用到TIM4计时器,开启后自动配置PD0,PD1引脚。开发板自带的USB接口,使用PA11,PA12引脚。开发板自带的ESP8266WIFI模块接口,对应USART2,使用PA2、PA3引脚。开发板的原理图与其他功能详见附件。
为了使各个模块更加独立,本系统开发时勾选了CubeMx下
project manager
/Code generator
/generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral
。设置以后每个外设都将生成独立的源码,方便管理。
图2. Stm32CubeMx中Stm32F103C8的引脚和Debug设置
虚拟USB串口用于单片机与PC的通信。配置完成后,可由PC上的串口通信助手打开与单片机的连接,并收发调试信息。
虚拟USB串口功能需要打开Connectivity
的USB Device(FS)
,并在Middleware
中打开USB虚拟串口:Communication Device Class (Virtual Port Com)
图3. 虚拟USB串口设置。
配置成功后,将stm32开发版的microUSB接口连接PC后,可以在串口调试助手中看到相应的COM并可以打开连接。
修改usbd_cdc_if.c
,重写USB串口的接收函数CDC_Receive_FS
,使其将收到的数据转发给huart2串口(ESP8266)。USB串口的发送函数CDC_Transmit_FS
保持默认。
在usbd_cdc_if.c中重写USB串口的接收函数CDC_Receive_FS
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
usbRxLen=Len[0];
for (int i=0;i<usbRxLen;i++){
UserRxBufferFS[i]=Buf[i];
}
CDC_Transmit_FS(UserRxBufferFS,usbRxLen);
HAL_UART_Transmit_DMA(&huart2 ,(uint8_t*)UserRxBufferFS,usbRxLen); //send to usart2
// generated code:
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
/* USER CODE END 6 */
}
单片机重启后有时候需要重新插拔USB接口,才能让windows系统重新检测串口连接状态,在调试时非常不方便。因此我们可以写一个模拟USB重新插拔的函数,代替手动插拔的操作。其原理是通过拉低再拉高D+引脚的电平,模拟插拔的动作。注意:模拟插拔的函数必须放在USB初始化之前,因为该函数内将USB相关引脚配置为GPIO模式,而USB初始化函数会在之后进行其他配置。
在usb_device.c中实现USB_Plug_Init
void USB_Plug_Init(void){
GPIO_InitTypeDef GPIO_InitStruct;
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET);
/*Configure GPIO pin : PA12 */
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//pull up PA12 then pull down again to simulate usb unplug and plug
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12,GPIO_PIN_RESET);
HAL_Delay(65);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_12,GPIO_PIN_SET);
HAL_Delay(65);
}
在MX_USB_DEVICE_Init的开头调用USB_Plug_Init
void MX_USB_DEVICE_Init(void)
{
/* USER CODE BEGIN USB_DEVICE_Init_PreTreatment */
USB_Plug_Init();
/* USER CODE END USB_DEVICE_Init_PreTreatment */
/* Init Device Library, add supported class and start the library. */
....
}
ESP8266模块用于建立与PC的无线连接。本系统中配置了两种连接模式。一种是ESP8266以AP热点模式启动,作为服务器建立自身的wifi。PC作为客户端连入该wifi后,可由网络调试助手连接服务器并发送数据,ESP8266可以收到多个客户端发送的数据,以及向指定的客户端发送数据。第二种方式是ESP8266以Station模式启动,作为客户端加入其他wifi。当PC和ESP8266位于同意wifi下时,利用网络调试助手打开TCP Server后,ESP8266可以通过PC的ip地址连接PC开设的服务器,并于服务器进行收发通讯。在该模式下ESP8266将打开透传模式,将所有USART2串口收到的数据直接发送到服务器。
通过修改main.c中ESP初始化函数的参数,可以选择芯片启动后的两种连接模式。用户也可以通过USB串口发送AT指令来手动配置ESP8266。
本系统采用ESP-01型号,如图3所示,EN和RST引脚需要接10k上拉电阻。由于Stm32开发板与ESP模块需要互传数据,因此收发引脚应对应相接。Stm32开发板的RXD引脚应接ESP的TXD引脚,TXD引脚接ESP的RXD引脚。由于ESP8266在发送大量数据时可能会产生较大的工作电流和功耗,因此建议采用外部稳压3.3v电源单独供电。本文采用STM32开发板的3.3v引脚进行供电,经测试可以收发少量数据。
本文采用的Stm32F103C8开发板上提供了ESP8266的专用接口,按照开发板原理图对应接线即可。该接口占用PA2和PA3引脚,默认对应USART2串口。
配置Usart2引脚,模式设为Asynchronous
,并打开global interrupt
。由于需要用到DMA收发,还需要手动设置DMA收发通道,如图5所示。
图5. ESP8266所需的USART2串口配置。
该USART串口除了收发数据外,还需要传输AT指令
(尤其是接收)。默认的DMA接收、中断接收和堵塞接收都只支持固定长度的接收,因此在接收不定长的AT指令时,一旦指令长度和预设长度不符,将发生发生错误:
为避免这一问题,需要实现任意长度的数据接收功能(参考博客)。思路:连续接收结束后会进入空闲中断。可以自定义空闲中断的处理方式,连续接收结束后,在空闲中断回调函数中处理缓存中的数据,并重置寄存器。
为了使用空闲中断,在USART2的初始化函数中使能空闲中断: __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE)
。之后,在USART2的中断处理中加入空闲中断处理,调用自己定义的回调函数。
usart.c
中修改MX_USART2_UART_Init
函数
void MX_USART2_UART_Init(void)
{
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART2_Init 2 */
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE); //
/* USER CODE END USART2_Init 2 */
}
在USART2的中断处理中加入空闲中断处理,调用自己定义的回调函数
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
if(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE) != RESET) // 空闲中断
{
USART2_DMA_RxCpltCallback(); // 调用自定义回调函数
}
/* USER CODE END USART2_IRQn 0 */
HAL_UART_IRQHandler(&huart2);
/* USER CODE BEGIN USART2_IRQn 1 */
/* USER CODE END USART2_IRQn 1 */
}
此外还需要自己实现中断回调函数,此处命名为USART2_DMA_RxCpltCallback
。在DMA接收结束时,会进入空闲中断,此时调用该回调函数以处理接收到的数据。首先重置寄存器,然后将收到的数据发送到usb串口,并清空接收缓存。除此之外,还需要重写收发错误回调函数USART2_UART_ErrorCallback
,从而在发生预料之外的错误时重新打开DMA接收功能,否则USART2串口将无法再次接收数据。该函数是__weak函数,在没有重写时将由默认函数代替。
usart.c
中定义所需的变量
volatile uint8_t recv_end_flag; //接收完成标记
volatile uint8_t rx_len; //接收数据长度
extern uint8_t USART2_RxBuffer[]; //usart2 receive buffer
usart.c
中实现USART2_DMA_RxCpltCallback
void USART2_DMA_RxCpltCallback(){
//used for receive data of any length
uint32_t temp;
__HAL_UART_CLEAR_IDLEFLAG(&huart2);//清除标志
temp = huart2.Instance->SR; //清除寄存器SR,读取SR寄存器可以实现清除SR寄存器的功能
temp = huart2.Instance->DR; //读取数据寄存器中的数据
HAL_UART_DMAStop(&huart2); //
temp = hdma_usart2_rx.Instance->CNDTR;// 获取DMA中未传输的数据个数 rx_len = USART2_RxBUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到已经接收的数据数量
recv_end_flag = 1; // 接受完成标志位置1
CDC_Transmit_FS(USART2_RxBuffer,sizeof(USART2_RxBuffer)); // send data to usb
memset(USART2_RxBuffer,0,sizeof(USART2_RxBuffer)); // reset buffer
rx_len=0;//清除计数
recv_end_flag=0;//清除接收结束标志
HAL_UART_Receive_DMA(&huart2,USART2_RxBuffer,USART2_RxBUFFER_SIZE);// 启动DMA接收
}
usart.c
中重写USART2_UART_ErrorCallback
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart2)
{
__HAL_UNLOCK(huart);
memset(&huart2,0,sizeof(UART_HandleTypeDef) );
HAL_UART_MspDeInit(&huart2);
MX_DMA_Init();
MX_USART2_UART_Init();
HAL_UART_Receive_DMA(&huart2,(uint8_t*)USART2_RxBuffer,USART2_RxBUFFER_SIZE);
}
}
配置USART2串口后,还需要针对ESP8266的功能进行封装,例如初始化、处理AT指令、向外发送数据等。ESP8266.h
中还需要指定ESP所用到的UART串口和相应的接收缓存。
ESP8266.h
中提供了一系列接口,并在此处定义ESP所用的usart接口组、缓存大小、缓存。移植时需要修改此处的定义。
//ESP8266.h
#ifndef INC_ESP8226_H_
#define INC_ESP8226_H_
#include "stm32f1xx_hal.h"
#define ESP_USART huart2 //define the uart group used by ESP8266
#define ESP_BUFFER_SIZE USART2_RxBUFFER_SIZE // define the buffer size
#define ESP_RxBuffer USART2_RxBuffer
void ESP_Send_cmd(char cmd[],uint32_t waittime, uint32_t delay);//发送AT
void ESP_Send_Data_TT(uint8_t* data, uint16_t len); //透传模式发送任意数据
void ESP_Send_Data_4Bytes(uint8_t* data);//非透传模式发送4字节
void ESP_Init(uint8_t mode);//初始化
void ESP_Init_TT();//打开透传
#endif /* INC_ESP8226_H_ */
在main函数中调用ESP_Init(1)
,可以将esp配置为station模式。默认连接的wifi和密码需要自己修改,TCP服务器地址(PC)192.168.0.196,端口8080。默认自动开启透传。调用ESP_Init(2)
将会配置ESP为AP模式(服务器模式),wifi名:TCP_Server,密码12345678,TCP地址192.168.4.1,端口8080。
调用ESP_Send_cmd
可以向ESP8266发送AT指令,并根据参数执行等待和延时。
DHT22温湿度传感器可同时测量温度和湿度数据,精确到小数点后一位。传感器采用单总线模式传输数据,占用Stm32一个GPIO引脚。接收数据时,先由Stm32发送一个激活信号,然后将GPIO引脚设置为接收,收到DHT22回应后,读取40位的数据。前0-15位为温度数值10,16-31位为湿度数据10,32-39位为校验位。前32位数据相加后应与最后8位数据相等。DHT22的测量周期应大于2s(系统中为3s),每次测量时返回的是上一次测量的数据。
DHT22温湿度传感器的接线如图6所示。因为传感器内置上拉电阻,数据引脚可直接连接Stm32引脚,Vcc接3.3v-6v电压。本系统采用stm32的3.3v引脚供电,经测试传感器工作正常。
图6. DHT22接线示意图。
DHT22采用单总线方式传输数据,因此CubeMx中只需要配置一个GPIO引脚(图7)。默认GPIO模式设为输出:Output open drain
。
DHT22相关功能的实现和接口分别在DHT22.c
、DHT22.h
中定义。
DHT22.h
定义了引脚和错误代码,以及提供了以下接口:
DHT22_Init()
:初始化DHT引脚,设为输出uint8_t DHT22_Get_Data(uint16_t *temp,uint16_t *humi)
:从头开始读取数据,包括向DHT发送开始接收数据的信号,获取响应,读取、校验数据void ONE_WIRE_Start()
:发送开始接收数据的信号uint8_t ONE_WIRE_Response()
:获取DHT的响应uint64_t DHT22_ReadData(void)
: 读取40位的原始数据void DHT22_THMsg(uint16_t Temp, uint16_t Humi, char THMsg[30])
:根据相应的温度适度数据,生成固定格式的字符串信息供调试使用。格式:Temp:00000000, Humi:00000000\r\n
//DHT22.h
#include "stm32f1xx_hal.h"
// pin
#define DHT_PORT GPIOA
#define DHT_PIN GPIO_PIN_6
// error codes
#define DHT_ERR_RESPONSE_FAILED 1
#define DHT_ERR_READ_BIT_FAILED 2
#define DHT_ERR_DATA_CHKSUM_FAILED 3
// function prototypes
void DHT22_Init(); //initialization function
void ONE_WIRE_Start(); //Start communication
uint8_t ONE_WIRE_Response(); //Sensor response for start command
void DHT22_THMsg(uint16_t Temp, uint16_t Humi, char THMsg[30]); //generate message string
uint8_t DHT22_Get_Data(uint16_t *temp,uint16_t *humi); //function available for user to use
uint64_t DHT22_ReadData(void); // read original data
此处将展示系统通过模式1方式与PC交换数据的过程。
按图8接线。将PC连接到指定wifi,打开网络调试助手,设置本地主机端口并开启TCP Server。
图8. 系统实物图。
将wifi名称和密码预先写好在ESP8266.c
中的对应指令中,编译并烧录到单片机中。
将USB连接到PC,在XCOM中打开串口连接,即可看到单片机发送至PC的调试信息,如图9所示。
与此同时,网络调试助手也能看到单片机成功连接TCP Server的信息,并且每3秒会收到一个4字节的数据,前2字节对应温度数据,后2字节对应湿度(图10)。例如第一条数据中,温度312,16进制即为0138,湿度477,16进制为01DD。数据一致,证明系统运行正常。
PC作为TCP Server也可以通过网络调试助手发送信息到单片机,单片机会将收到的信息转发到USB串口。
图9. XCOM串口调试助手收到的调试信息。
图10. 网络调试助手收到的调试信息。
在XCOM中可以向USB串口发送AT指令,单片机会将AT指令转发至ESP8266。首先发送+++
(不需要\r\n字符)关闭透传模式,否则无法接受AT指令。然后发送AT
,可以收到返回的信息AT OK
,即测试AT指令正确。发送AT+RST
可以看到ESP成功重置,如图11所示。调试信息中的乱码是由于关闭透传之后,4字节的温湿度数据会被转发到USB串口,对应的ASCLL码乱码。
按下stm32开发板的复位按钮,能够重置单片机并让程序从头运行。此时,需要在复位后重新打开串口,才能接收到ESP8266的初始化信息。