赞
踩
前些阵子参加了广东省电赛,因为疫情的原因比赛不能在线下进行,甚至连回学校调试也不行,于是乎就水了一个省三。
备赛的时候,队长给我布置了这个小项目,说有可能会用在比赛的作品中,但实际就不知道了,所以在这里分享一下代码。
这个需求对性能要求不高,像人见人爱、人手一个的STM32F103C8T6也能胜任,但因为我没带回家,所以用了野火的霸道V2开发板。
这块开发板用的是STM32F103ZET6芯片。
温湿度传感器玩过单片机的小伙伴都应该特别熟悉了,在这里就不再赘述了。但还是放几张时序图解释一下。
总体来说DHT11是串行通信,只用一根数据线进行通信,通信的形式有点像IIC,都是先给一个响应信号,然后传感器应答后开始传输数据,直到从机或主机发送结束信号。
下面是主机发送响应信号的时序。
从机接收到主机的响应信号后会按以下时序产生应答信号,表示接受响应。
实际发送的数据都是以一低一高的形式表示,“0”和“1”的区别在于高电平的持续时间,利用这一点主机可以区分“0”和“1”比特。
下面是接收一个字节和接收温湿度数据的代码:
/* * 从DHT11读取一个字节,MSB先行 */ static uint8_t DHT11_ReadByte ( void ) { uint8_t i, temp=0; for(i=0;i<8;i++) { /*每bit以50us低电平标置开始,轮询直到从机发出 的50us 低电平 结束*/ while(DHT11_Dout_IN() == RESET); /*DHT11 以26~28us的高电平表示“0”,以70us高电平表示“1”, *通过检测 x us后的电平即可区别这两个状 ,x 即下面的延时 */ DHT11_DELAY_10US(4); //延时x us 这个延时需要大于数据0持续的时间即可 if(DHT11_Dout_IN() == SET)/* x us后仍为高电平表示数据“1” */ { /* 等待数据1的高电平结束 */ while(DHT11_Dout_IN() == SET); temp|=(uint8_t)(0x01<<(7-i)); //把第7-i位置1,MSB先行 } else // x us后为低电平表示数据“0” { temp&=(uint8_t)~(0x01<<(7-i)); //把第7-i位置0,MSB先行 } } return temp; } /* * 一次完整的数据传输为40bit,高位先出 * 8bit 湿度整数 + 8bit 湿度小数 + 8bit 温度整数 + 8bit 温度小数 + 8bit 校验和 */ uint8_t DHT11_Read_TempAndHumidity(DHT11_Data_TypeDef *DHT11_Data) { /*输出模式*/ DHT11_Mode_Out_PP(); /*主机拉低*/ DHT11_Dout_0; /*延时18ms*/ DHT11_DELAY_MS(18); /*总线拉高 主机延时30us*/ DHT11_Dout_1; DHT11_DELAY_10US(3); //延时30us /*主机设为输入 判断从机响应信号*/ DHT11_Mode_IPU(); /*判断从机是否有低电平响应信号 如不响应则跳出,响应则向下运行*/ if(DHT11_Dout_IN() == RESET) { /*轮询直到从机发出 的80us 低电平 响应信号结束*/ while(DHT11_Dout_IN() == RESET); /*轮询直到从机发出的 80us 高电平 标置信号结束*/ while(DHT11_Dout_IN() == SET); /*开始接收数据*/ DHT11_Data->humi_int= DHT11_ReadByte(); DHT11_Data->humi_deci= DHT11_ReadByte(); DHT11_Data->temp_int= DHT11_ReadByte(); DHT11_Data->temp_deci= DHT11_ReadByte(); DHT11_Data->check_sum= DHT11_ReadByte(); /*读取结束,引脚改为输出模式*/ DHT11_Mode_Out_PP(); /*主机拉高*/ DHT11_Dout_1; /*检查读取的数据是否正确*/ if(DHT11_Data->check_sum == DHT11_Data->humi_int + DHT11_Data->humi_deci + DHT11_Data->temp_int+ DHT11_Data->temp_deci) return SUCCESS; else return ERROR; } else return ERROR; }
因为可燃气体检测和雨滴感应的传感器原理较相似,所以放到一起讲了。
可燃气体传感器用的是MQ2。
下面是雨滴模块的样子(简单到连名字都没有)
两个模块都是用模拟信号通信,所以使用STM32的ADC外设就可以轻松读取数据。
根据STM32的用户手册知道我们使用的这款芯片有3个ADC外设,为了让单片机能更快处理转换模拟信号,所以使用了双ADC模式;为了让我们能随时调整采样的时间,我还使用了ADC外设的外部触发功能;为了保证数据传输的效率我还使用了DMA进行传输。
解释一下双ADC模式,通俗讲就是正常是一个ADC处理2个模拟信号的转换,现在2个ADC分别负责2个模拟信号的转换,并且是硬件控制同时开始转换的,因此效率会高很多。
STM32支持的双ADC模式有好几个,具体可参考用户文档,在这里我们使用最常用的同步规则模式即可。
但需要注意的是双ADC模式只能使用ADC1和ADC2外设
接下来放核心代码吧,对于STM32的ADC玩法还挺多的,计划在另外写一篇文章来讲一下。
adc.c文件:
#include "adc/adc.h" static uint32_t ADC_RAW_DATA = 0; void ADC_Config(void) { /* DMA初始化 */ DMA_InitTypeDef DMA_Init_Struct = {0}; RCC_AHBPeriphClockCmd(DMA_PERI_CLK_PORT, ENABLE); DMA_DeInit(ADC_DMA_CHAL); DMA_Init_Struct.DMA_PeripheralBaseAddr = ADC1_DR_Address; // 外设基地址 DMA_Init_Struct.DMA_MemoryBaseAddr = (uint32_t)&ADC_RAW_DATA; // 内存基地址 DMA_Init_Struct.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设到内存 DMA_Init_Struct.DMA_BufferSize = 1; // 数据块数量 DMA_Init_Struct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 关闭外设地址自增 DMA_Init_Struct.DMA_MemoryInc = DMA_MemoryInc_Disable; // 关闭内存地址自增 DMA_Init_Struct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; // 字 DMA_Init_Struct.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; // 字 DMA_Init_Struct.DMA_Mode = DMA_Mode_Circular; // 循环模式 DMA_Init_Struct.DMA_Priority = DMA_Priority_High; // 优先级高 DMA_Init_Struct.DMA_M2M = DMA_M2M_Disable; // 关闭内存至内存模式 DMA_Init(ADC_DMA_CHAL, &DMA_Init_Struct); DMA_Cmd(ADC_DMA_CHAL, ENABLE); DMA_ClearFlag(DMA1_FLAG_TC1); /* 初始化计时器 */ TIM_TimeBaseInitTypeDef TIM_BaseInit_Struct = {0}; RCC_APB1PeriphClockCmd(TIM_PERI_CLK_PORT, ENABLE); TIM_BaseInit_Struct.TIM_ClockDivision = TIM_CKD_DIV1; // 输入捕获才用到,默认值即可 TIM_BaseInit_Struct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数 TIM_BaseInit_Struct.TIM_Period = TIM_PERIOD_VAL; // 周期,即在(1 / 2kHz) * (TIM_PERIOD_VAL + 1) = CONV_TIME(ms)后产生上溢,这也是ADC的触发周期 TIM_BaseInit_Struct.TIM_Prescaler = (36000 - 1); // 预分频,即分频后的时钟频率为72Mhz / (35999 + 1) = 2kHz TIM_BaseInit_Struct.TIM_RepetitionCounter = 0; // 不使用重装载寄存器 TIM_TimeBaseInit(TIM_PORT, &TIM_BaseInit_Struct); TIM_SelectOutputTrigger(TIM_PORT, TIM_TRGOSource_Update); // 使能输出触发 TIM_ClearFlag(TIM_PORT, TIM_FLAG_Update); /* 初始化ADC */ ADC_InitTypeDef ADC_Init_Struct = {0}; RCC_APB2PeriphClockCmd(MQ2_ADC_PERI_CLK_PORT | WaterSensor_ADC_PERI_CLK_PORT, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC 6分频,即采样频率为72 / 6 = 12MHz ADC_Init_Struct.ADC_ContinuousConvMode = DISABLE; // 关闭连续采样模式 ADC_Init_Struct.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐 ADC_Init_Struct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; // ADC1外部触发,由计时器3 TRGO事件负责 ADC_Init_Struct.ADC_Mode = ADC_Mode_RegSimult; // 同步扫描模式 ADC_Init_Struct.ADC_NbrOfChannel = 1; // 1个通道 ADC_Init_Struct.ADC_ScanConvMode = DISABLE; // 关闭扫描模式 ADC_Init(MQ2_ADC_PORT, &ADC_Init_Struct); ADC_Init_Struct.ADC_ContinuousConvMode = DISABLE; // 关闭连续采样模式 ADC_Init_Struct.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐 ADC_Init_Struct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // ADC2关闭外部触发 ADC_Init_Struct.ADC_Mode = ADC_Mode_RegSimult; // 同步扫描模式 ADC_Init_Struct.ADC_NbrOfChannel = 1; // 1个通道 ADC_Init_Struct.ADC_ScanConvMode = DISABLE; // 关闭扫描模式 ADC_Init(WaterSensor_ADC_PORT, &ADC_Init_Struct); ADC_DMACmd(MQ2_ADC_PORT, ENABLE); // 使能ADC DMA功能 ADC_Cmd(MQ2_ADC_PORT, ENABLE); ADC_Cmd(WaterSensor_ADC_PORT, ENABLE); ADC_ExternalTrigConvCmd(MQ2_ADC_PORT, ENABLE); // 使能ADC1外部触发转换 ADC_ExternalTrigConvCmd(WaterSensor_ADC_PORT, ENABLE); // 使能ADC2外部触发转换 /* 转换时间 (1 / 12MHz) * (55.5 + 12.5) ≈ 5.7us */ ADC_RegularChannelConfig(MQ2_ADC_PORT, MQ2_ADC_CHAL, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(WaterSensor_ADC_PORT, WaterSensor_ADC_CHAL, 1, ADC_SampleTime_55Cycles5); ADC_ResetCalibration(MQ2_ADC_PORT); // 复位ADC1 while(ADC_GetResetCalibrationStatus(MQ2_ADC_PORT)); ADC_StartCalibration(MQ2_ADC_PORT); // 校正ADC1 while(ADC_GetCalibrationStatus(MQ2_ADC_PORT)); ADC_ResetCalibration(WaterSensor_ADC_PORT); // 复位ADC2 while(ADC_GetResetCalibrationStatus(WaterSensor_ADC_PORT)); ADC_StartCalibration(WaterSensor_ADC_PORT); // 校正ADC2 while(ADC_GetCalibrationStatus(WaterSensor_ADC_PORT)); TIM_Cmd(TIM_PORT, ENABLE); // 开启计时器,开启ADC转换 } uint16_t ADC_GetMQ2RawData(void) { uint32_t tmp = ADC_RAW_DATA; return (uint16_t)(tmp & 0x0000ffff); } uint16_t ADC_GetWaterSensorRawData(void) { uint32_t tmp = ADC_RAW_DATA; return (uint16_t)(tmp >> 16); }
adc.h文件:
/* 相关配置 */ #define CONV_TIME 100 // 采样周期,单位ms /* 其他宏定义 */ #define TIM_PERIOD_VAL (CONV_TIME * 2 - 1) /* 管脚宏定义 */ #define TIM_PORT TIM3 #define TIM_PERI_CLK_PORT RCC_APB1Periph_TIM3 #define DMA_PERI_CLK_PORT RCC_AHBPeriph_DMA1 #define ADC1_DR_Address ((uint32_t)0x4001244C) #define ADC_DMA_CHAL DMA1_Channel1 void ADC_Config(void); uint16_t ADC_GetMQ2RawData(void); uint16_t ADC_GetWaterSensorRawData(void);
OLED屏幕应该也有很多小伙伴玩过了。
这个屏幕的通信方式有挺多种的,比如说8080时序、IIC、SPI,具体是哪种通信方式要看买的时候商家的说明。
我买的这个是IIC的接口,我使用了硬件IIC对其通信。
简单贴一下代码:
碍于篇幅就只放初始化和传输一个字节的代码吧。
/** * @brief 初始化OLED针脚 * @param None * @retval None */ static void OLED_GPIO_Init(void) { GPIO_InitTypeDef GPIO_Init_Struct; RCC_APB2PeriphClockCmd(OLED_PERI_CLK, ENABLE); GPIO_Init_Struct.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_Init_Struct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init_Struct.GPIO_Pin = OLED_SCL_PIN; GPIO_Init(OLED_SCL_PORT, &GPIO_Init_Struct); GPIO_Init_Struct.GPIO_Pin = OLED_SDA_PIN; GPIO_Init(OLED_SDA_PORT, &GPIO_Init_Struct); RCC_APB1PeriphClockCmd(IIC_PERI_CLK, ENABLE); I2C_InitTypeDef I2C_Init_Struct; I2C_Init_Struct.I2C_Ack = I2C_Ack_Enable; // 使能ACk I2C_Init_Struct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 7位从机地址 I2C_Init_Struct.I2C_ClockSpeed = 1000000; // 速度100kHz I2C_Init_Struct.I2C_DutyCycle = I2C_DutyCycle_2; I2C_Init_Struct.I2C_Mode = I2C_Mode_I2C; I2C_Init_Struct.I2C_OwnAddress1 = 0x00; // STM32自己的地址,任意值即可 I2C_Init(IIC_PORT, &I2C_Init_Struct); I2C_Cmd(IIC_PORT, ENABLE); } /** * @brief 初始化SSD1306 * @param None * @retval None */ void OLED_Init(void) { OLED_GPIO_Init(); OLED_SendCmd(0xAE);//--turn off oled panel OLED_SendCmd(0x00);//---set low column address OLED_SendCmd(0x10);//---set high column address OLED_SendCmd(0x40);//--set start line address Set Mapping RAM Display Start Line (0x00~0x3F) OLED_SendCmd(0x81);//--set contrast control register OLED_SendCmd(0xCF); // Set SEG Output Current Brightness OLED_SendCmd(0xA1);//--Set SEG/Column Mapping 0xa0左右反置 0xa1正常 OLED_SendCmd(0xC8);//Set COM/Row Scan Direction 0xc0上下反置 0xc8正常 OLED_SendCmd(0xA6);//--set normal display OLED_SendCmd(0xA8);//--set multiplex ratio(1 to 64) OLED_SendCmd(0x3f);//--1/64 duty OLED_SendCmd(0xD3);//-set display offset Shift Mapping RAM Counter (0x00~0x3F) OLED_SendCmd(0x00);//-not offset OLED_SendCmd(0xd5);//--set display clock divide ratio/oscillator frequency OLED_SendCmd(0x80);//--set divide ratio, Set Clock as 100 Frames/Sec OLED_SendCmd(0xD9);//--set pre-charge period OLED_SendCmd(0xF1);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock OLED_SendCmd(0xDA);//--set com pins hardware configuration OLED_SendCmd(0x12); OLED_SendCmd(0xDB);//--set vcomh OLED_SendCmd(0x40);//Set VCOM Deselect Level OLED_SendCmd(0x20);//-Set Page Addressing Mode (0x00/0x01/0x02) OLED_SendCmd(0x02);// OLED_SendCmd(0x8D);//--set Charge Pump enable/disable OLED_SendCmd(0x14);//--set(0x10) disable OLED_SendCmd(0xA4);// Disable Entire Display On (0xa4/0xa5) OLED_SendCmd(0xA6);// Disable Inverse Display On (0xa6/a7) OLED_SendCmd(0xAF);//--turn on oled panel OLED_SendCmd(0xAF); /*display ON*/ OLED_Clear(); OLED_Set_Pos(0,0); } /** * @brief 向SSD1106写入一个字节 * @param dat 要写入的数据/命令 * @param cmd 数据/命令标志 0,表示命令;1,表示数据 * @retval */ static void OLED_WR_Byte(uint8_t dat, uint8_t cmd) { // FlagStatus bitstatus = RESET while (I2C_GetFlagStatus(IIC_PORT, I2C_FLAG_BUSY)); // 检查I2C总线是否繁忙 I2C_GenerateSTART(IIC_PORT, ENABLE); // 打开IIC总线 // ErrorStatus status = ERROR, ERROR是个枚举类型, 值为0 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 检查总线是否打开 I2C_Send7bitAddress(IIC_PORT, IIC_ADDR, I2C_Direction_Transmitter); // 配置STM32的IIC设备自己的地址,每个连接到IIC总线上的设备都有一个自己的地址,作为主机也不例外。 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 检查地址是否发送 if (cmd) I2C_SendData(IIC_PORT, 0x40); // 进入数据模式 else I2C_SendData(IIC_PORT, 0x00); // 进入命令模式 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)); // 等待发送数据完成 I2C_SendData(IIC_PORT, dat); //发送数据 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)); // 等待发送数据完成 I2C_GenerateSTOP(IIC_PORT, ENABLE); // 关闭IIC总线 }
应队长的要求这个小项目用了FreeRTOS系统,但实际上除了用到最基本的任务调度外,其他的功能都没有用到。
至于FreeRTOS系统的移植网上都有好多的教程了,这里就不赘述了。
简单说主任务做的就是一段时间后切换OLED屏幕的显示内容(因为一块OLED屏幕显示不了所有传感器的数据)
简单贴一下代码:
static void MainTask(void* params) { uint8_t status = 0; while(1) { if (status) { DHT11_Read_TempAndHumidity(&dht11_data); OLED_ShowTempAndHumi(dht11_data.temp_int, dht11_data.temp_deci, dht11_data.humi_int, dht11_data.humi_deci); printf("Displaying temperature and humidity\n"); } else { OLED_ShowPpmAndWaterLevel(MQ2_GetPPM(), WaterSensor_GetWaterLevel()); printf("Displaying PPM and water level\n"); } status = !status; vTaskDelay(2000); } } static void AppInitTask(void* params) { taskENTER_CRITICAL(); // 进入临界段 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); USART_Config(); MQ2_Init(); WaterSensor_Init(); ADC_Config(); TIMER_Init(); DHT11_Init(); OLED_Init(); printf("Init completed\n"); vTaskDelay(100); xTaskCreate(MainTask, "MainTask", 1024, NULL, 5, NULL); printf("Main task created\n"); vTaskDelete(NULL); // 删除这个任务 taskEXIT_CRITICAL(); // 退出临界段 } /** * @brief 主函数 * @param None * @retval None */ int main(void) { BaseType_t xReturn = pdPASS; xReturn = xTaskCreate(AppInitTask, "AppInitTask", 1024, NULL, 5, NULL); if (xReturn == pdPASS) vTaskStartScheduler(); // 开启任务调度 else while(1); }
总的来说,这个项目还是蛮适合对STM32已经入门然后想进阶的小伙伴。
项目包含了对各种常见模块的代码撰写,每种模块对应的通信协议、用到的外设也不尽相同;项目还涉及了对FreeRTOS系统的基本移植和使用,对于想进阶STM32的小伙伴是非常好的。
链接:https://download.csdn.net/download/JackieCoo/86506760
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。