概述
前些阵子参加了广东省电赛,因为疫情的原因比赛不能在线下进行,甚至连回学校调试也不行,于是乎就水了一个省三。
备赛的时候,队长给我布置了这个小项目,说有可能会用在比赛的作品中,但实际就不知道了,所以在这里分享一下代码。
固件编写
这个需求对性能要求不高,像人见人爱、人手一个的STM32F103C8T6也能胜任,但因为我没带回家,所以用了野火的霸道V2开发板。
这块开发板用的是STM32F103ZET6芯片。
DHT11温湿度传感器
概述
温湿度传感器玩过单片机的小伙伴都应该特别熟悉了,在这里就不再赘述了。但还是放几张时序图解释一下。
总体来说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可燃气体传感器和雨滴传感器
概述
因为可燃气体检测和雨滴感应的传感器原理较相似,所以放到一起讲了。
可燃气体传感器用的是MQ2。
下面是雨滴模块的样子(简单到连名字都没有)
两个模块都是用模拟信号通信,所以使用STM32的ADC外设就可以轻松读取数据。
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屏幕
概述
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总线
}
main.c文件
概述
应队长的要求这个小项目用了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