基于STM32的光强测量仪设计(硬件篇)
本期我们旨在设计一款光强测量仪,项目要求如下:准确度[removed]3级成本[removed]
从零开始的ESP32天气时钟(3)——设计界面并美化
由于尝试过TFT屏幕不能显示中文,因此我们如果想要显示中文的话就需要导入新的字库,但是尝试过比较麻烦,而且我们所需要使用的中文字库量并不是很大,于是我们选择使用图片来代替字库,将中文作为图片打印到TFT屏幕上。 我暂时想的UI设计如下,分为七块区域分别是:标题、地区、天气、日期、星期、时间以及自己的DIY区域。天气部分由于心知天气免费版所获得的天气信息只有地区、天气、气温三样信息,而天气我们可以暂时总结为晴天、多云、雨天。因此我们需要制作这三类相关的图片。 我们从网上找到一些免费的开源图片,打开PS将其转换为我们的TFT屏幕需要的大小(我选择的是50x50)之后打开我们的取模软件(私信LCD取模软件后发送链接)将图片转换为16进制字符串信息。 注意蓝色框内的设置,否则会出错。 点击保存之后就可以获得我们的数组信息。 将数组信息复制下来,注意的是需要将保存到的信息中的const修饰符去掉以及加上PROGMEM使数据以代码的形式保存下来。 到我们的代码中,我们使用TFT_eSPI库中的pushImage函数打印我们的图片。就可以在指定位置打印我们的图片啦,注意图片的长度和宽度都要一一对应。我们查看 pushImage的函数声明可以发现,我们调用的红框的函数,但是这个函数的数据部分是没有加 const的所以我们原本的数据如果加上const就会报错(以后有机会讲讲C++的函数重载) 同样的我们做出其他部分的图片信息,例如星期,以及地名还有天气情况。 我们可以使用PS(主要是限制一下文字大小)制作我们的时间信息 和 上一期获取天气的代码 类似(文末给出全部代码)void getWeatherData() {
HTTPClient http;//创建连接结构体
http.begin(weatherApiUrl);//尝试连接
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
String weatherData = http.getString();//获取HTTP响应
Serial.println(weatherData);
DynamicJsonDocument doc(1024);
deserializeJson(doc, weatherData);//解析HTTP
weather.City = doc["results"][0]["location"]["name"].as[removed]();//城市
weather.description = doc["results"][0]["now"]["code"].as[removed]();;//天气
weather.temperature = doc["results"][0]["now"]["temperature"].as[removed]();//
}
else {
Serial.println("Failed to fetch weather data");
}
/*
根据天气情况打印相对应天气图片,这个天气代码对应的值在心知天气的官网
查看API文档获得
*/
if(weather.description == 0 || weather.description == 1 )
{
tft.pushImage(190,0,50,50,qingtian);
}
if(weather.description >= 4 && weather.description [removed]= 10 && weather.description <= 18 )
{
tft.pushImage(190,0,50,50,yutian);
}
/*
根据城市打印城市地点(目前只做了杭州)
*/
if(weather.City.equals("杭州"))
{
tft.pushImage(110,0,80,50,hangzhou);
}
http.end();
/*
获得时间信息
*/
struct tm timeinfo;
time_t now = timeClient.getEpochTime();
gmtime_r(&now, &timeinfo);
tft.setCursor(0,65);
tft.setTextSize(4);//设置大字体
/*
打印日期
*/
tft.print(timeinfo.tm_mon+1);//月份从0~11,所以要+1
tft.print('.');
tft.print(timeinfo.tm_mday);//答应日期
switch(timeinfo.tm_wday)//根据星期来打印日期
{
case 0:tft.pushImage(130,50,110,50,zhouri);break;
case 1:tft.pushImage(130,50,110,50,zhouyi);break;
case 2:tft.pushImage(130,50,110,50,zhouer);break;
case 3:tft.pushImage(130,50,110,50,zhousan);break;
case 4:tft.pushImage(130,50,110,50,zhousi);break;
case 5:tft.pushImage(130,50,110,50,zhouwu);break;
case 6:tft.pushImage(130,50,110,50,zhouliu);break;
}
tft.pushImage(0,0,110,50,tianqishizhong);//打印标题
Serial.println(weather.description);
Serial.println(weather.City);
}
同样的,我们也需要打印时间信息,时间信息和上期也几乎没有区别,但是把串口输出换成了屏幕。我们通过设置不同的字体使时间的显示有层次感。void displaytime() {
tft.setTextSize(2);
tft.setCursor(10, 110);
tft.fillRect(0,110,240,130,TFT_WHITE); // 清空屏幕以便显示新时间
// 获取当前时间
int hours = timeClient.getHours();
int minutes = timeClient.getMinutes();
int seconds = timeClient.getSeconds();
tft.setTextSize(5);//设置大字体
// 打印小时
tft.print(hours);
// 打印分钟
tft.print(":");
tft.print(minutes);
tft.setTextSize(4);//设置小字体
// 打印秒钟
tft.print(":");
tft.println(seconds);
}
在主函数中我们需要设置我们的刷新时间以及获取天气情况的频率。我们设置一个计时。但是后来想想还是选择加入定时器的使用。 我们在库管理导入ESP32Timerintterupt库,并写入头文件中。#include [removed]
ESP32Timer ITimer(1); // 使用定时器 1
const uint64_t timer_period = 3600000000; // 1 小时的微秒数
void IRAM_ATTR onTimer() {
//获取天气数据
getWeatherData();
//或者更新显示的时间
displaytime();
}
并且在主函数中启动定时器,写在void setup()中 ITimer.attachInterruptInterval(timer_period, onTimer); // 设置定时器中断
ITimer.start();
这样子我们就可以实现一个小时更新一次天气数据了。
求项目推荐
家人们有没有什么好的项目推荐一下呀
从零开始的ESP32天气时钟(2)——获取天气信息并打印
上一期使用了ESP32点亮TFT屏幕以及获取时间之后,这期我们使用ESP32获取天气情况。 首先搜索心知天气,进入官网并且注册后,我们可以申请免费版(20次/分钟的申请频率)我们申请的免费版后在下方会有密钥,这个密钥就是我们要用的我们按照这样子的方式把我们的私钥填入api.seniverse.com/v3/weather/now.json?key=Your_private_key&location=beijing&language=zh-Hans&unit=c,可以直接将这个复制入浏览器上方的搜索栏观察是否有效。这样子就成功申请到啦,我们的准备工作也就结束了。做好上述准备工作之后我们来编写我们的代码首先我们需要新安装HTTPClient和ArduinoJson库用以进行HTTP连接以及对接收端 Json字符串进行解析。因为我们的HTTP请求回来时是一段Json字符串需要解析后方可以使用 接着设置我们的网站地址就是上面那个 为了更方便地存储天气信息,我们定义一个结构体来存放我们的数据// 定义结构体来存储天气数据
struct WeatherData {
int City;//城市代码
int description;//天气代码
double temperature;//温度
};
接着我们编写获取天气的函数,主要为发送HTTP请求,如果响应成功则解析收到的内容。void getWeatherData() {
HTTPClient http;//创建连接结构体
http.begin(weatherApiUrl);//尝试连接
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
String weatherData = http.getString();//获取HTTP响应
DynamicJsonDocument doc(1024);
deserializeJson(doc, weatherData);//解析HTTP
weather.City = doc["results"][0]["location"]["name"].as[removed];//城市
weather.description = doc["results"][0]["now"]["text"].as[removed]>;;//天气
weather.temperature = doc["results"][0]["now"]["temperature"].as[removed]();//
} else {
Serial.println("Failed to fetch weather data");
}
http.end();
}
我们将城市代码,天气情况以及温度存入我们的结构体中。之后打印我们的时间,天气等信息。void displayWeather() {
tft.setCursor(10, 50);
timeClient.update();
tft.println(timeClient.getFormattedTime());
tft.print("City: ");
tft.println(weather.City);
tft.print("Description: ");
tft.println(weather.description);
tft.print("Temperature: ");
tft.print(weather.temperature);
tft.println(" °C");
}
从零开始的ESP32天气时钟(1)——点亮TFT并连接Wifi
前段时间在网上看见有关ESP系列的天气时钟,于是这几天决定复刻一个,材料非常简单, 1.54寸的TFT显示屏还有一块ESP32 。 由于我的TFT显示屏是8针TFT屏幕,因此我们首先要配置相关文件,我们打开Ardunio,在“库管理”中下载TFT_eSPI库,之后在项目库地址打开TFT_eSPI文件夹中的User_Setup.h进行配置 我们的屏幕信号为ST7789,因此我们需要将7789的宏注释解除掉。并且注释掉原有的宏定义。 往下滑动鼠标,可以看到有关 TFT 显示屏 大小的定义,我们也要选择相对应的高度以及宽度。 由于我们使用的是ESP32 ,所以需要将下面ESP8266的宏给去掉。 之后开启ESP32对应的宏并修改我们的引脚位置以对应 TFT 显示屏 . 最后在我们使用的时候,如果TFT显示屏是⑧针的话,一定记得要把BLO引脚接高!!! 商家给我的例程说这个不用接,最后整整浪费我六个小时的时间。 修改好设置后之后,我们输入以下的代码#include [removed]
TFT_eSPI tft = TFT_eSPI();
void setup() {
pinMode(19, OUTPUT);//启动BLK背光
digitalWrite(19, HIGH);
Serial.begin(115200);
tft.init();//初始化
tft.fillScreen(TFT_WHITE);
}
烧录我们进的程序(注意引脚一定一定一定千万千万不要连错) 点亮成功! 连接Wifi 首先我们需要添加WiFi的头文件。 之后写入下面的代码#include[removed]#include [removed]
TFT_eSPI tft = TFT_eSPI();
const char * wifiname = "name";//wifi名称
const char * wifipass = "password";//wifi密码
void setup() {
pinMode(18, OUTPUT);//启动BLK背光
digitalWrite(18, HIGH);
Serial.begin(115200);
tft.init();//初始化
tft.fillScreen(TFT_WHITE);
WiFi.begin(wifiname,wifipass);
while (WiFi.status() != WL_CONNECTED) {//等待连接
delay(1000);
tft.fillScreen(TFT_BLACK); // 清空屏幕
tft.setCursor(10, 50);
tft.println("Connecting to WiFi...");
}
tft.fillScreen(TFT_BLACK); // 清空屏幕
tft.setCursor(10, 50);
tft.println("Connected to WiFi");
}
注意WiFi的头文件大小写不要写错了,然后烧录我们的代码。 接着我们在库管理中使用NTPClien这个库,连接上NTP服务器,并且导入WiFiUdp.h库,使得我们可以进行网络通信。#include[removed]#include [removed]
#include[removed]#include [removed]
TFT_eSPI tft = TFT_eSPI();
const char * wifiname = "name";//wifi名称
const char * wifipass = "password";//wifi密码
const char* ntpServerName = "cn.pool.ntp.org";
const int utcOffset = 8; // 中国时区偏移量(+8小时为例)
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, ntpServerName, utcOffset * 3600);
void setup() {
pinMode(18, OUTPUT);//启动BLK背光
digitalWrite(18, HIGH);
Serial.begin(115200);
tft.init();//初始化
tft.fillScreen(TFT_WHITE);
WiFi.begin(wifiname,wifipass);
while (WiFi.status() != WL_CONNECTED) {//等待连接
delay(1000);
tft.fillScreen(TFT_BLACK); // 清空屏幕
tft.setCursor(10, 50);
tft.println("Connecting to WiFi...");
}
tft.fillScreen(TFT_BLACK); // 清空屏幕
tft.setCursor(10, 50);
tft.println("Connected to WiFi");
timeClient.begin();
timeClient.update();
// 打印当前时间
Serial.println(timeClient.getFormattedTime());
tft.setRotation(1); // 旋转一下屏幕不然看着难受
}
void loop() {
// put your main code here, to run repeatedly:
timeClient.update();
// 在循环中可以使用 timeClient 来获取当前时间
String currentTime = timeClient.getFormattedTime();
// 清空屏幕
tft.fillScreen(TFT_BLACK);
// 设置文本属性
tft.setTextSize(2);
tft.setTextColor(TFT_WHITE);
tft.setCursor(10, 50);
// 在屏幕上显示时间
tft.println(currentTime);
// 这里可以加入其他代码
delay(1000);
}
记得修改Wifi密码和账号。这样子我们就可以获取实时时间啦。
基于STM32F407的FreeRTOS学习笔记(12)—— 利用直达任务通知模拟任务间通信
在上一期内容中我们简单的介绍了任务通知的几个函数以及简单的使用了任务通知来实现两个信号之间的通信。 本期我们将利用任务通知来模拟三种方式的任务间通信。 信号量 在我们介绍信号量的文章中介绍过,信号分为二进制信号量和计数信号量(点击可跳转链接) 接着我们使用任务通知来模拟这两项功能。 二进制信号量可以看作长度为1的队列,我们不关心其值为多少,只关心它的状态。 在直达任务通知中我们可以用xTaskNotifyGive来模拟二进制信号量的释放以及ulTaskNotifyTake()来模拟二进制信号量的读取。 在ulTaskNotifyTake()中需要注意的是,我们需要设置一个参数用来确定我们模拟的是二进制信号量还是计数信号量。 代码测试void Mid_Task(void * pvParameters)//参数为 void * pvParameters
{
while(1)
{
if(KEY_Scan(0)==1)
{
printf("Key_Press\r\n");
xTaskNotifyGive(High_Handler);//传入任务函数句柄,模拟信号量释放
}
}
vTaskDelay(10);
}
void High_Task(void * pvParameters)
{
BaseType_t err;
while(1)
{
err = ulTaskNotifyTake(pdFALSE,10);//读取后清零,模拟二进制信号量
if(err == pdTRUE)
{
printf("Recieve Message!\r\n");
}
vTaskDelay(10);
}
}
模拟二进制信号量成功。 之后,我们将接收的函数中的pdFALSE修改为pdTRUE,这样子我们就可以模拟我们的计数信号量了。 这里就不作演示了,但是要注意的是,只用这样子的模拟二进制信号量也要注意优先级反转问题,关于优先级反转的问题可以参考公众号中的关于二进制信号量的文章:基于STM32F407的FreeRTOS学习笔记(8)——优先级反转问题以及如何解决(互斥信号量) 事件组 合理的运用RTOS中的事件组可以很好的处理许多事件,在事件组的介绍中我们说过,我们常用的事件组可以做到24位事件位。而在直达任务通知中,我们也同样可以指定某些位的改变来实现事件组的效果。 我们可以修改xTaskNotify中的eAction来将通知值作为事件组,修改特定位来实现事件位的效果。 代码测试void Mid_Task(void * pvParameters)//参数为 void * pvParameters
{
int i = 0;
while(1)
{
if(KEY_Scan(0)==1)
{
printf("Key_Press keynumber : 1\r\n");
xTaskNotify( (TaskHandle_t) High_Handler,//目标任务句柄
(uint32_t) 0x04,//第二位 00000100
(eNotifyAction) eSetBits);//位设置模式,模拟事件组
}
if(KEY_Scan(0)==2)
{
printf("Key_Press keynumber : 2\r\n");
xTaskNotify( (TaskHandle_t) High_Handler,//目标任务句柄
(uint32_t) 0x08,//第三位 00001000
(eNotifyAction) eSetBits);//位设置模式,模拟事件组
}
}
vTaskDelay(10);
}
void High_Task(void * pvParameters)
{
BaseType_t err;
uint32_t number;//存放通知值
uint32_t Value; //模拟事件组
while(1)
{
err = xTaskNotifyWait( (uint32_t) 0x0000,//不清理
(uint32_t) 0xffff,//清理当前
(uint32_t*) &number,//接收任务值
(TickType_t) 10 );//等待事件
Value = Value | number ; //获得事件位
if((Value&(0x08+0x04)) == (0x08+0x04))
{
printf("KEY1 and KEY2 have Pressed\r\n");
Value = 0;//事件组清零
}
vTaskDelay(10);
}
}
基于STM32F407的FreeRTOS学习笔记(11)—— 直达任务通知
在前面的学习中例如信号量、队列中经常会出现提示:在许多情况下,“任务通知”可以提供二进制信号量的轻量级替代方案。 那么本期内容着重于介绍什么是任务通知以及如何使用。 首先我们去FreeRTOS的官网,阅读开发者文档中关于“任务通知”的介绍。 我们可以了解到,任务通知是由一条(或多条)任务"通知状态"以及一个通知值 组成。 任务通知是直接发送给任务的,传递通知值以及改变通知状态。任务通知如同任务一样也可以进入阻塞状态进行等待。 灵活的运用任务通知可以快捷的替代信号量、队列以及事件组等任务间的通讯。 使用任务通知 FreeRTOS中有好几个发送任务通知的函数,我们一一介绍他们的用处以及区别。 首先是xTaskNotify和xTaskNotifyIndexed,它们分别用来发送任务通知和像任务通知数组发送通知(自V10.4.0一条任务可以有多条任务通知) xTaskToNotify是需要通知的任务句柄,uxIndexToNotify在 xTaskNoti fy是没有的,用在任务通知数组中的,ulValue则是我们需要传递的32位通知值,最后eAction是用来确定我们传递通知值的方式。 例如xTaskNotifyGive,可以用来替代二进制信号量使用,也等效于eAction设置为 eIncre ment,通知值自增1。 下面我们来看一下等待任务通知的函数。测试代码 测试代码非常简单,我们定义一个轮询按钮按下的函数,如果按钮按下,我们就用任务通知向我们的任务函数发送一个通知值。任务函数则一直等待任务值,如果收到通知,就打印通知值。、void Mid_Task(void * pvParameters)//发送任务通知函数
{
int i = 0;
while(1)
{
if(KEY_Scan(0)==1)
{
printf("Key_Press\r\n");
xTaskNotify( (TaskHandle_t) High_Handler,//任务函数的句柄
(uint32_t) i,//通知值
(eNotifyAction) eSetValueWithOverwrite );//用覆盖的方式传递
i++;
}
}
vTaskDelay(10);
}
void High_Task(void * pvParameters)//接收任务通知函数
{
uint32_t NotifyNumber;//存放任务通知值
BaseType_t err;
while(1)
{
err = xTaskNotifyWait( (uint32_t) 0,//不清除位
(uint32_t) 0,//不清除位
(uint32_t * ) &NotifyNumber,
(TickType_t) 10 );//等待10
if(err == pdTRUE)
{
printf("Recieve Notify is : %d\r\n",NotifyNumber);
}
vTaskDelay(10);
}
}
效果展示
基于STM32F407的FreeRTOS学习笔记(10)—— 虚假的Flag?真正的事件组!
在介绍二进制信号量时曾经讲过,二进制信号量可以代替我们裸机开发中的标志位来使用。在裸机开发中我们使用标志位来表示某个事件是否发生,并且其他程序利用标记位的状态来判断程序是否可以继续进行。但是这种大量使用标记位的情况会导致代码的逻辑异常的复杂。 虽然使用二进制信号量可以很好的实现标志位的实现以及相应的任务安排,但是二进制信号量并不适用于大量的标志位。因为一个二进制信号量只能表示一个事件,假如我们的程序有大量的事件那有没有办法不用二进制信号量可以很好的管理这些事件呢? 这就是本期介绍的内容:事件位和事件组 在FreeRTOS中我们把一个用作判断事件是否发生的情况作为事件位,用0或1表示。它可以用来表示一个事件是否发生。比如函数Test是否被调用,用户是否按下按键等等事件。 FreeRTOS 通过相对应的宏定义来确定事件组的长度(包含多少事件位)例如config_16_BIT_TICKS为0的话则代表一个事件组可以包含24位事件位。 使用事件组 和之前的其他内容一样,使用事件组的准备也要包含相关头文件、定义相对应的宏定义、调用创建函数。 事件组的创建函数 非常简单,只需要定义一个事件组的句柄来接收事件组创建函数的返回值即可。事件组的长度前面说过利用宏定义来确定时间组长度。 等待事件组的函数和信号量相似,设置我们需要等待事件发生的位并且设置超时时间。但是注意的是我们选择需要等待的位不能设置为0 第三个参数用来设置等到事件位触发后是否清空数据位。 其中的第四个参数xWaitForAllBits则是用来确定位之间的与或关系,例如如果我的位设置是0x03即第0和第1位,如果xWaitForAllBits是pdTRUE则需要第0和第1位同时为1,如果是pdFalse则第0和第1只要有一个事件触发就执行。 测试实验 首先我们创建一个事件组,宏定义中定义这个事件组总共有24个事件位。 EventHandler = xEventGroupCreate();
接着我们创建一个按键检测任务,当按钮一按下时,将事件组第0位置1,当按钮二按下时,将事件组第1位置1;void Low_Task(void * pvParameters)//参数为 void * pvParameters
{
while(1)
{
if(KEY_Scan(0)==1)
{
xEventGroupSetBits(EventHandler,1<<0);//1<<0是0x01
}
if(KEY_Scan(0)==2)
{
xEventGroupSetBits(EventHandler,1<<1);//1<[removed]
基于STM32F407的FreeRTOS学习笔记(9)——想要多少个定时器就要多少个就要多少个
在嵌入式编程中,定时器是一个非常重要且强大的功能,用来帮我们定时性的调用中断服务函数来帮助我们处理程序。定时器通常是用硬件来实现的,例如STM32F407就有8个硬件定时器。 而在FreeRTOS中则可以用软件实现定时器。大大的扩展了定时器的数量。 阅读FreeRTOS开发者文档我们可以知道,软件定时器的回调函数会在定时器服务函数中执行。 同时,软件定时器的回调函数中不能调用然后可以导致阻塞的函数例如vTaskDelay()等等,包括等待信号量的函数这些也会造成阻塞。 FreeRTOS会使用队列向定时器服务任务发送命令,这个队列就是定时器命令队列。 创建软件定时器 #FreeRTOS# 在API引用文档中,我们可以查询有关创建软件定时器创建的函数。内容有许多,但是还是总归是分三个步骤:包含相关头文件、启动相关的宏、配置软件定时器。 软件定时器配置时也有三个参数需要注意,一个定时器的定时时间,一个是设置定时器重复使用还是一次项,最后需要注意的是传入调用函数的句柄。 定义相关的回调函数与定时器句柄。Timer_Handler = xTimerCreate
( (const char * const) "xTimerCreate",
(const TickType_t) 500,//500ms一次
(const UBaseType_t) pdTRUE,//重复
(void * const) 1,//定时器ID
(TimerCallbackFunction_t) CallBacl );//中断服务函数句柄
接着我们定义一个轮询函数,当按键按下时,我们就开启定时器void Low_Task(void * pvParameters)//参数为 void * pvParameters
{
while(1)
{
if(KEY_Scan(0)==1)
{
xTimerStart(Timer_Handler,100);//开始定时器传入句柄以及最大等待时间
}
}
}
在回调函数中我们让LED灯进行翻转void CallBacl( TimerHandle_t xTimer)
{
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_10);
}
同样的,在官方的API文档中我们还可以看到许多和软件定时器有关的内容,在这里不一一介绍,有感兴趣的小伙伴可以去官网查看API使用说明。
基于STM32F407的FreeRTOS学习笔记(8)——优先级反转问题以及如何解决(互斥信号量)
前面几期我们介绍过队列、二进制信号量以及计数信号量。但是在使用二进制信号量的时候会有一种优先级反转问题的出现,简而言之就是低优先级任务因为无法及时释放信号量而导致等待信号量发生的高优先级任务迟迟无法进行。 众所周知,FreeRTOS的各任务的运行顺序是由任务的优先级决定的,优先级高的任务比优先级低的任务先执行。 假设我们有三个任务:任务H,任务M,任务L,分别代表高优先级,中优先级以及低优先级。任务H和任务M同时被挂起,正在等待某一个事件的发生,同时任务H和任务L使用同样的全局资源(意味着当任务L正在占用全局资源时任务H的执行需要等待任务L执行完释放信号量)当任务L运行时占用了全局资源。任务H的事件被触发后由于其高优先级任务H获得CPU的使用权当任务H需要使用全局资源时,由于任务L还没有使用完全局资源,因此任务H被挂起等待任务L的信号量释放即使用函数xSemaphoreTake(SemaphoreHandler,portMAX_DELAY);等待信号量此时任务L恢复工作任务M的事件触发,此时任务L在等待任务M的结束,而然后任务H此时也在等待着任务M的任务结束。因此这段时间的任务M优先级高于任务H,这种现象就是优先级反转。等任务M执行完,任务L继续执行直到释放信号量,任务H得以继续运行。 因此使用信号量就会导致优先级 反转 的出现,打破原有的任务运行顺序,这在RTOS系统中应当是尽量避免的。 问题解决 为了解决二进制信号量可能带来的优先级反转现象,FreeRTOS中有一种特殊的二进制信号量——互斥信号量也就是互斥锁。 官方的开发者文档中介绍了互斥锁的存在,互斥锁实际上是一个包含了优先级继承机制的二进制信号量。任务在使用资源时则相当于手持一块令牌,其他没有这块令牌的任务无法就使用资源这就是所谓的互斥。当任务结束使用资源时,也就会返回所对应的令牌。 当两个任务使用相同的信号量时,为了避免前面介绍的优先级反转现象,于是优先级高的任务会把持有令牌的低优先级任务的优先级提升到和自己一样的情况,这样子就可以导致中间出现的中优先级任务抢占CPU资源,使得优先级出现反转。 相当于如图,将之后执行的低优先级任务的有限制强制抬高到与自己同一等级,颇有一种富家少爷为了吃葡萄而包下葡萄园的故事感。 这样子的做法可以有效地防止优先级反转现象的出现,也可以使已经出现的优先级反转得到更快的结束。 注意,互斥锁一定一定一定不能在中断中使用,因为中断无法使用延时函数来阻塞事件。 使用互斥锁 在FreeRTOS中有两种方法创建互斥锁,分别是动态创建和静态创建,我们主要介绍一下动态创建互斥锁。 API文档中关于互斥锁的内容很多,主要是介绍互斥锁以及说明使用互斥锁使用的一些细节。 信号量的释放和获取则和二进制信号量一样,参考二进制信号量文章 我们来测试一下互斥锁。 测试代码 我们需要三个不同优先级的任务,用来测试优先级反转的情况。 我们分别定义高优先级任务,中优先级任务以及低优先级任务。 启动相关宏定义。先测试二进制信号量void High_Task(void * pvParameters);//高优先级任务
void Mid_Task(void * pvParameters);//中优先级任务
void Low_Task(void * pvParameters);//低优先级任务
void Scan(void * pvParameters);
#define START_TASK_PRIO 1
TaskHandle_t High_Handler;
TaskHandle_t Start_LED_Handler;
TaskHandle_t Mid_Handler;
TaskHandle_t Low_Handler;
TaskHandle_t Scan_Handler;
xSemaphoreHandle TaskSemaphoer_Handler;
void Start_LED(void * pvParameters)
{
taskENTER_CRITICAL();
TaskSemaphoer_Handler = xSemaphoreCreateBinary();//创建二进制信号量
if(TaskSemaphoer_Handler!=NULL)
{
printf("Semaphore Create Successfully\r\n");
}
xTaskCreate((TaskFunction_t )High_Task,//任务函数
(char * )"v",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 2,//优先级2
(TaskHandle_t * )&High_Handler);//任务函数句柄
xTaskCreate((TaskFunction_t )Mid_Task,//任务函数
(char * )"Mid_Task",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 1,//优先级1
(TaskHandle_t * )&Mid_Handler);//任务函数句柄
xTaskCreate((TaskFunction_t )Low_Task,//任务函数
(char * )"Low_Task",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 0,//优先级0
(TaskHandle_t * )&Low_Handler);//任务函数句柄
vTaskSuspend(Mid_Handler);//挂起中优先级任务
vTaskSuspend(High_Handler);//挂起高优先级任务
taskEXIT_CRITICAL();
vTaskDelete(NULL);
}
void FreeRTOS_Init()
{
xTaskCreate((TaskFunction_t )Start_LED,
(char * )"starttask",
(configSTACK_DEPTH_TYPE) 128,
(void* ) NULL,
(UBaseType_t )START_TASK_PRIO,
(TaskHandle_t * )&Start_LED_Handler);
vTaskStartScheduler();
}
接着我们按照会出现优先级反转的情况编写测试代码。 首先挂起高优先级和中优先级任务。 低优先级任务持续打印运行信息,当运行到5次时,恢复高优先级任务持续打印信息,高优先级任务打印三次后等待低优先级任务发送信号量。 当低优先级任务再执行5次后高恢复中优先级任务,再次执行5次后发送信号量示意高优先级任务继续运行。 中优先级任务执行3次后挂起自身。void Low_Task(void * pvParameters)//参数为 void * pvParameters
{
int Low_number = 0;
while(1)
{
printf("Low_Task Runing 1111\r\n");
Low_number++;
if(Low_number == 5)
{
vTaskResume(High_Handler);//恢复高优先级
}
if(Low_number == 10)
{
vTaskResume(Mid_Handler);//恢复中优先级任务
}
if(Low_number == 15)
{
xSemaphoreGive(TaskSemaphoer_Handler);//释放信号量
}
}
}
void Mid_Task(void * pvParameters)//参数为 void * pvParameters
{
int Mid_number = 0;
while(1)
{
printf("Mid_Task Runing 2222\r\n");
Mid_number++;
if(Mid_number == 3)
{
vTaskSuspend(NULL);//挂起自身
}
}
}
void High_Task(void * pvParameters)
{
int High_number = 0;
while(1)
{
printf("High_Task Runing 3333\r\n");
High_number++;
if(High_number==3)
{
xSemaphoreTake(TaskSemaphoer_Handler,portMAX_DELAY);//等待低优先级任务释放信号量
}
}
}
可以看见和预想的一样,高优先级的任务被中优先级任务所挤兑。 之后我们把代码中的二进制信号量换成互斥锁。 可以看到,中优先级的任务根本没有办法实现优先级反转跳到高优先级去。 因此善于使用互斥锁,避免优先级反转现象的出现有利于FreeRTOS系统任务调度顺序的正确性,防止出现意外错误。
基于STM32F407的FreeRTOS学习笔记(7)——计数信号量
本期在二进制信号量的基础上介绍计数信号量什么是计数信号量 计数信号量顾名思义是用来计数的信号量,相比于二进制信号量,计数信号量的并不只有两种状态。用官方的开发者文档中的话来说,计数信号量可以看作长度大于1的队列,我们并不关心其中的内容而是关系队列是否为空。如何创建计数信号量 官方的参考文档中提供了两种创建方式(动态和静态)我们使用动态创建方式。调用xSemaphoreCreateCounting函数 其中包含了两个参数,一个是最大计数量还有一个是初始计数量。 创建一个SemaphoreHandler_t类型的句柄变量用以接收返回值。释放和获取信号量 释放和获取信号量和上一期二进制信号量的释放和获取方式一样。均是调用 xSemaphoreGive释放信号量以及调用 xSemaphoreTake获取信号量。 但是计数信号量则多了一个可以调用的函数。 调用这个函数我们就可以获得计数值啦。代码编写测试流程 我们做两个实验,首先是使用一个LED函数,函数每翻转一次就向计数信号量释放一次信号。 第二个函数轮询计数信号量,当计数信号量的数量比一半多时,使另一个LED也开始进行翻转并同样释放信号量。当计数信号量到达最大数时,关闭第二个灯的翻转。大体思路 第一个LED灯翻转,发送信号量。定义一个轮询函数用来时刻检测信号量状况,当信号量到达一定数量时恢复LED2任务的运行,当信号量满时清空信号量列表并挂起LED2 任务的挂起与恢复可以参考这期。代码编写 首先是任务启动函数,在这个函数中我们要创建一个计数信号量并且启动其他的相关任务函数。
void Start_LED(void * pvParameters)
{
taskENTER_CRITICAL();
LED_SemaphoreHandler = xSemaphoreCreateCounting(20,0);//最大计数20,初始0
if(LED_SemaphoreHandler!=NULL)
{
printf("Semaphore Create Successfully\r\n");
}
xTaskCreate((TaskFunction_t )LED_TOG,//任务函数
(char * )"LED_TOG",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 1,//优先级1
(TaskHandle_t * )&LED_TOG_Handler);//任务函数句柄
xTaskCreate((TaskFunction_t )LED_TOG2,//任务函数
(char * )"LED_TOG2",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 2,//优先级1
(TaskHandle_t * )&LED_TOG2_Handler);//任务函数句柄
xTaskCreate((TaskFunction_t )CountTest,//任务函数
(char * )"GetCount",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 0,//优先级1
(TaskHandle_t * )&GetCount_Handler);//任务函数句柄
taskEXIT_CRITICAL();
vTaskSuspend(LED_TOG2_Handler);
vTaskDelete(NULL);
}
其次LED函数的内容非常简单,检测信号量是否创建(指针不为空)如果指针不为空则翻转LED,并且释放信号量。(注意第二个LED的函数不释放信号量防止释放两次信号量)
void LED_TOG(void * pvParameters)//参数为 void * pvParameters
{
while(1)
{
if(LED_SemaphoreHandler!=NULL)
{
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_9);
xSemaphoreGive(LED_SemaphoreHandler);
}
vTaskDelay(500);//延迟500ms
}
}
void LED_TOG2(void * pvParameters)//参数为 void * pvParameters
{
while(1)
{
if(LED_SemaphoreHandler!=NULL)
{
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_10);
}
vTaskDelay(500);
}
}
在循环检测函数中,我们定义一个count来接收计数信号量的数量。接着当信号量大于10时我们恢复LED2函数的运行(可以多次恢复,只有一次效果)。 当信号量为20时,我们先暂停LED1函数的运行防止我们清空信号量的时候LED1又在释放信号量。之后通过不断的获取信号量来清空信号量,因为信号量本质就是队列,之后恢复他们的运行。
void CountTest(void * pvParameters)
{
while(1)
{
BaseType_t count;
if(LED_SemaphoreHandler!=NULL)
{
count = uxSemaphoreGetCount(LED_SemaphoreHandler);
if(count >= 9 )
{
vTaskResume(LED_TOG2_Handler);//恢复函数2
}
if(count >= 20 )
{
vTaskSuspend(LED_TOG_Handler);//先挂起函数1
while(count != 0)
{
xSemaphoreTake(LED_SemaphoreHandler,10);
count = uxSemaphoreGetCount(LED_SemaphoreHandler);
}
vTaskResume(LED_TOG_Handler);//恢复函数1
vTaskSuspend(LED_TOG2_Handler);//挂起函数2
}
}
vTaskDelay(10);
}
}
##freeRTOS#
干货超硬核,定位略模糊:嘉立创PCB实战指南新书测评
前言 前段时间嘉立创的工作人员向我介绍了他们发布的PCB新书:《从设计到量产:电子工程师PCB智造实战指南》 据嘉立创介绍这是他们十数年来的经验总结,旨在为减少我们在设计过程中由于经验问题而造成的“踩坑”。 博主收到书之后,最近也是仔细的读了一遍并且发表一下自己对这本书的看法。这本书讲了什么?总体上来说这本书的章节分布还是合理的,系统性地讲解PCB从材料选择、核心设计规则到关键生产工艺(钻孔、线路、阻焊、字符、外形、表面处理)的全链条知识。 该书总计13章节,第1章PCB常用软件介绍了PCB设计软件和PCB制造软件,为后续设计打下基础。第2章介绍决定PCB性能和成本的关键因素——基板材料。第3章概述了PCB生产的整个工艺流程和关键参数,让读者对制造环节有宏观认识。第4章~第8章深入剖析了PCB制造中几个最关键的工艺环节的设计要点和制造过程包括钻孔、线路、阻焊、字符标识和外形设计。第9章专门讨论了PCB焊盘表面处理(如喷锡、沉金、OSP、沉银等)的不同选择及其优缺点。第10章单独介绍了FPC的基础知识、生产和注意事项。第11章到第13章则是介绍了嘉立创EDA、嘉立创DFM以及嘉立创CAM的使用。 细节上来说,这本书确实称得上“干货满满”四个字,书中有大量的图、表信息以及非常非常多的专业术语,并且各章节内容几乎都是干货,没有太多的“废话”内容。 专业内容非常详尽,博主在阅读过程中确实收获了很多很多先前接触不到的知识。 在很多需要丰富设计经验支撑的地方,这本书会提供一些错误样例供我们参考,我认为演示错误示范是非常好的积累经验的方式(我们对做什么事情是错误的印象更深)同样的,这本书在绝大部分涉及到设计、制造类内容时,很贴心的附上了视频演示。这是非常大的一个加分项,相比于静态的图片,动态的视频更能让我们了解到PCB制造和设计中的细节知识。缺点和不足毫无疑问这本书的专业知识之硬核,实用性无疑是非常非常高的,但是博主也谈一下自己对这本书的另外一些批判性看法,谨代表个人观点。 其实我最开始听到这本书名字《从设计到量产:电子工程师PCB智造实战指南》的时候,想当然的认为它是类似于项目集,类似手把手教你立创EDA设计与制造PCB的全流程这种。但是收到书本并仔细阅读之后,我对这本书的定位更倾向于“教材”。 部分章节的练习题强化了该书的教材属性,其专业深度也足以胜任教材角色。这种双重特性带来不同阅读体验:作为工具指南,详尽内容反而模糊重点,通读全文略显枯燥;作为专业教材,口语化表达则带有明显的实用指南特征,这带来了一种矛盾感。 而且最重要的是,我认为无论从哪个角度它都缺少了一个压轴菜:实战Demo/课程设计,虽然书名是实战指南,但是更多的实战知识都是分布式的,作为一名读者很期待有一个手把手设计制作的教学。总结 总的来说这本书还是非常不错的,专业知识丰富,我虽然不是专业的PCB工程师,但是仍旧有非常多的收获,而且在阅读过程中也从中看到了自己以前犯的很多错误的影子例如板子的加热温度、PCB过孔规则(单面孔怎么画)等等,同时关于这些知识尤其是PCB制造方面的相关,它也是我遇到的第一本成系统性的去介绍各个工艺的书,涵盖了几乎我能想到和想不到的所有范畴。 我记得大二期间有一门选修课叫:PCB设计,使用的是AD来进行设计了一个NE555的流水灯。实操PCB设计但是缺没有专门的教材来介绍PCB的工艺流程、设计步骤和要点等,可以说这门课相当粗糙,而这本书就很适合作为这类课的教材辅助使用。 因此嘉立创这本书我还是非常推荐对此方面感兴趣和没有系统培训的工程师来阅读一遍以增加工创经验和阅历的,即便目前感觉此书还是有些许瑕疵和需要改进的地方,但是仍旧是能够收获很多实用的知识的。#嘉立创PCB#读起来怪怪的?
基于STM32F407的FreeRTOS学习笔记(6)——二进制信号量(别在傻乎乎的使用flag变量
信号量( Semaphore)也被称为信号灯。有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量(来自百度百科) 简而言之,信号量就是在全局中表示共享资源状态的量。例如一个停车场,其中的车位就是共享资源。每当有车辆进进出出的时候,门口门卫总会统计出入车辆的数量,这就是信号量,我们可以通过信号量来获公共资源的信息(空余车位、已用车位) 而二进制信号量顾名思义只有0和1,例如电话亭的使用情况,当有人的时候其他人就无法使用电话亭。只有当电话亭空余的时候才能使用电话亭,而电话亭的使用状态则是二进制信号,电话亭本身则是共享资源。 在原本的裸机开发中我们通常会使用大量的标记符号并且在main函数中不断轮询该标记,这样子代码的逻辑就会异常复杂,而二进制信号量则可以代替这样子的作用,当任务在继续时二值信号量返回0,任务空闲时二进制信号量返回1,可以完美的替代如下这些标志变量。 在FreeRTOS中我们通常也会使用一个任务来专门轮询信号量,获得信号量的状态,实现信号量的同步。 除此之外我们的程序通常会有一个公共缓存区作为共享资源,每一个资源都可以使用公共缓存区的数据,即可以从中读取数据也可以写入数据。这个公共缓存区就像是停车场,车位有限,而我们则是根据信号量来控制这个停车场是否能够继续停下车辆。 在FreeRTOS的介绍中我们可以看到,而二进制信号量的可以看作只有一个项目的队列,用队列的空和满来代表信息。 导入我们关于信号量的头文件“semphr.h” 关于二进制信号量的API文档中,创建一个二进制信号量首先需要将相对应的宏,即configSUPPORT_DYNAMIC_ALLOCATION打开,接着创建一个SemaphoreHandle_t 的信号量句柄来接收该创建函数的返回值。
LED_SemaphoreHandler = xSemaphoreCreateBinary();
if(LED_SemaphoreHandler!=NULL)
{
printf("Semaphore Create Successfully\r\n");
}
接着我们在启动函数中写上该函数。这样子我们编译并烧录进我们的单片机。
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
#endif
跳转之后发现,其实这个函数就是创建一个项目大小为1 的队列,因此二进制信号量的本质就是队列。 在文档中找到获取信号量的函数,分别是xSemaphoreTake和xSemaphoreTakeFromISR,从名字中我们可以知道这两个函数分别是在普通函数与中断函数中获取信息量的。 可以看到,用法还是非常的简单,返回值是pdTRUE和pdFALSE,是用来判断信号量是否有用,即队列是否有空余。其中的参数xTicksToWait则是用来设置等待时间,在等待时间内阻塞以试图获得信号量。 最后我们看看释放信号量的函数 这个释放信号量,并不是说释放空间那种表示删除的意思,而是如最后表达的那样,发布信号量。简而言之其实也就是向队列中的项目发布数据。 所以正确的流程是:创建信号量,轮询检测信号量是否释放,释放信号量。代码检验 ##freeRTOS# 接下来检验一下我们的信号量。 我们先创建一个任务,轮询信号量并且一直等待信号量是否释放。如果检测到信号量则翻转LEDBaseType_t err;
while(1)
{
if(LED_SemaphoreHandler!=NULL)
{
err = xSemaphoreTake(LED_SemaphoreHandler,portMAX_DELAY);//一直等待信号量
if(err == pdTRUE)
{
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_10);//LED翻转
}
else
{
printf("No Semaphore\r\n");
}
}
vTaskDelay(10);
}
接着编写按钮函数,如果按下按钮则释放一个信号量。 if(key==2)
{
if(LED_SemaphoreHandler!=NULL)
{
err = xSemaphoreGive(LED_SemaphoreHandler);
printf("Give Semaphore Success\r\n");
}
else
{
printf("Give Semaphore Fail\r\n");
}
}
基于STM32F407的FreeRTOS学习笔记(4)——获取各任务运行时间及占用情况
前言 CPU工作的时候,各个任务运行会占用CPU的资源,在Windows系统中我们可以通过任务管理器来看各任务(进程)占用系统资源的情况。 那么,FreeRTOS怎么实现这个功能呢? 我们翻阅FreeRTOS官网,查询API文档,在内核控制函数部分找到了相关的函数。 文档指出实现运行时间功能需要配置外设定时器,即32板载定时器,计时器频率应为滴答计时器(1ms)的至少10倍。 传入参数为pcWriteBUffer,其实是一个char类型的数组用以存储相关信息。代码实例 我们现在工程上调用这个函数。char informationbuff[400];
void Get_info(void * pvParameters)
{
//vTaskGetRunTimeStats(informationbuff);
while(1)
{
if(KEY_Scan(0)==1)//按下按键1
{
memset(informationbuff,0,400);//清空数组内容
vTaskGetRunTimeStats(informationbuff);//获得运行时间
printf("%s\r\n",informationbuff);//打印运行时间
}
vTaskDelay(10);
}
}
上述任务的作用为检测按键,如果按键按下即尝试获得运行状态,并打印运行状态。 出现了如下错误,显示我们未定义该函数,我们利用Ctrl+F全局寻找这个函数定义在哪里。 FreeRTOS\FreeRTOS.axf: Error: L6218E: Undefined symbol vTaskGetRunTimeStats (referred from main.o).
F:\Code\STM32Code\STM32F407_FreeRtos\FreeRTOS\FreeRTOS\Source\tasks.c(4539) : void vTaskGetRunTimeStats( char * pcWriteBuffer )
F:\Code\STM32Code\STM32F407_FreeRtos\FreeRTOS\FreeRTOS\Source\tasks.c(4552) : * vTaskGetRunTimeStats() calls uxTaskGetSystemState(), then formats part
F:\Code\STM32Code\STM32F407_FreeRtos\FreeRTOS\FreeRTOS\Source\tasks.c(4557) : * vTaskGetRunTimeStats() has a dependency on the sprintf() C library
F:\Code\STM32Code\STM32F407_FreeRtos\FreeRTOS\FreeRTOS\Source\tasks.c(4567) : * through a call to vTaskGetRunTimeStats().
第一行内容,即为函数定义的位置,我们跳转过去查看其情况。#if ( ( configGENERATE_RUN_TIME_STATS == 1 ) && ( configUSE_STATS_FORMATTING_FUNCTIONS > 0 ) && ( configUSE_TRACE_FACILITY == 1 ) )
void vTaskGetRunTimeStats( char * pcWriteBuffer )
{
TaskStatus_t * pxTaskStatusArray;
UBaseType_t uxArraySize, x;
configRUN_TIME_COUNTER_TYPE ulTotalTime, ulStatsAsPercentage;
省略后续内容(防止说水字数)
我们看到了函数模型以及相关注释,从头中我们可以看出需要相关的宏定义,分别是configGENERATE_RUN_TIME_STATS、configUSE_STATS_FORMATTING_FUNCTIONS、configUSE_TRACE_FACILITY。 我们在FreeRTOSconfig.h文件(头文件都行,方便管理)中添加使能这三个宏。 再次运行,依旧报错,从报错内容来看,提醒我们如果将 configGE NERATE_RUN_TIME _STATS使能的话,我们也必须定义portCONFIGURE_TIMER_FOR_RUN_TIME_STATS这个启动函数,以及后面的一条报错,我们必须定义portGET_RUN_TIME_COUNTER_VALUE时间的返回值。
#ifndef portCONFIGURE_TIMER_FOR_RUN_TIME_STATS
(import) #error If configGENERATE_RUN_TIME_STATS is defined then portCONFIGURE_TIMER_FOR_RUN_TIME_STATS must also be defined. portCONFIGURE_TIMER_FOR_RUN_TIME_STATS should call a port layer function to setup a peripheral timer/counter that can then be used as the run time counter time base.
#endif /* portCONFIGURE_TIMER_FOR_RUN_TIME_STATS */
#ifndef portGET_RUN_TIME_COUNTER_VALUE
#ifndef portALT_GET_RUN_TIME_COUNTER_VALUE
(import) #error If configGENERATE_RUN_TIME_STATS is defined then either portGET_RUN_TIME_COUNTER_VALUE or portALT_GET_RUN_TIME_COUNTER_VALUE must also be defined. See the examples provided and the FreeRTOS web site for more information.
#endif /* portALT_GET_RUN_TIME_COUNTER_VALUE */
#endif /* portGET_RUN_TIME_COUNTER_VALUE *
启动函数即为外部定时器启动函数,返回值则是一个数用以计算时间。#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() configTIM_START()//定时器1提供时间统计的时基,频率为10K,即周期为100us
#define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTime//时基
extern volatile unsigned long long FreeRTOSRunTime;
我们定义这两个宏,本来这个括号是没加上去的,后来发现其调用的时候是代括号的,所以定义宏的时候不带括号就会出错 此外我们定义了long long 类型的变量用以存储我们的时间,加上extern表示这个变量的实际定义并不在头文件中,之所以加上volatile是因为我们的变量会在不同的文件以及中断中被修改(这种修改属于意外修改),加上volatile标志给系统提前吱会一声。 之后,我们去CUBEMX启动我们的定时器。 定时器我们选择定时器1,时钟源选择内部时钟,分频系数由于我们的单片机主频是168MHZ,因此我们选择168分频,这样子定时器频率即为1MHZ,溢出值我们选择为50,通过这样的设置我们定时器的频率就是20KHZ,是滴答定时器时钟的20倍。 FreeRTOSRunTime也可以定义在这里。 之后我们将刚才宏定义的启动函数进行定义,内容则是重置计数器并启动定时器。 完成这步之后,我们还需要在主函数中启用定时器1 的中断并且编写相应的中断服务函数,其内容为FreeRTOSRunTime递增。
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == htim1.Instance)
{
FreeRTOSRunTime++;
}
}
/* USER CODE END 4 */
之后我们运行程序观察串口的输出。 这样子我们就可以打印出各个程序运行时间以及占用系统资源的占比了。 ##freeRTOS#
基于STM32F407的FreeRTOS学习笔记(2)——任务的创建与删除
上一期配置完FreeRTOS的环境后,这一期记录自己关于任务创建的学习过程。 官方的API手册中有这些函数,xTaskCreate和xTaskCreateStatic分别是利用动态方法和静态方法创建任务。(动态和静态的区别之后再研究)vTaskDelete是删除任务,因为freeRTOS的任务内存空间存储在堆区,所以很像C语言的动态内存分配,任务使用和结束我们都应该创建和删除这些任务防止占用过多空间。 xTaskCreate的函数模型如下,参数内容总共有六项:任务函数的函数指针,任务函数的名称,任务函数所需堆栈空间,任务函数的类型,任务函数的优先级,以及任务函数的函数句柄 vTaskDelete的函数模型如下,参数内容为函数句柄,如果为NULL则删除该任务本身。 因此我们创建任务的步骤是:首先定义一个启动任务,该任务是为了启动我们的真正任务,因此在调用完一遍后要用vTaskDelete 中输入NULL删除启动函数本身。任务函数编写/*
LED1翻转
*/
void LED_TOG(void * pvParameters)//参数为 void * pvParameters
{
while(1)
{
printf("LED_TOG running\r\n");//串口打印运行信息
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_10);//LED1翻转
vTaskDelay(500);//延迟500ms
}
}
要注意的是vTaskDelay是FreeRTOS用来延时的函数。 之后我们要创建任务函数的启动函数
TaskHandle_t Start_LED_Handler;
void Start_LED(void * pvParameters)
{
xTaskCreate((TaskFunction_t )LED_TOG,//任务函数
(char * )"LED_TOG",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 1,//优先级1
(TaskHandle_t * )LED_TOG_Handler);//任务函数句柄
vTaskDelete(NULL);
}
我们创建启动任务的函数,将任务函数的函数指针,任务函数的名称,任务函数所需堆栈空间,任务函数的类型,任务函数的优先级,以及任务函数的函数句柄,填入vTaskCreate函数中,其中每个参数都使用了强制类型转换防止出现错误。 同样的方法,我们创建启动 启动函数的任务(有点绕口因为启动函数本身是一个任务)
void FreeRTOS_Init()
{
xTaskCreate((TaskFunction_t )Start_LED,
(char * )"Start_LED",
(configSTACK_DEPTH_TYPE) 128,
(void* ) NULL,
(UBaseType_t ) 0,
(TaskHandle_t * )Start_LED_Handler);
vTaskStartScheduler();//启动运行函数
}
这样子我们在主函数中添加刚刚定义的启动启动函数int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
KEY_Init();
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
FreeRTOS_Init();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
用上述的方法实现两个灯一起翻转 ##FreeRTOS# ##FreeRTOS# #FreeRTOS# 还是先编辑任务函数如下,并且定义其相关句柄TaskHandle_t LED_TOG2_Handler;
void LED_TOG2(void * pvParameters)//参数为 void * pvParameters
{
while(1)
{
printf("LED_TOG running\r\n");//串口打印运行信息
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_9);//LED0翻转
vTaskDelay(500);//延迟500ms
}
}
在任务启动函数中加入我们新建的任务。
void Start_LED(void * pvParameters)
{
xTaskCreate((TaskFunction_t )LED_TOG,//任务函数
(char * )"LED_TOG",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 1,//优先级1
(TaskHandle_t * )LED_TOG_Handler);//任务函数句柄
xTaskCreate((TaskFunction_t )LED_TOG2,//任务函数
(char * )"LED_TOG2",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 2,//优先级2
(TaskHandle_t * )LED_TOG2_Handler);//任务函数句柄
vTaskDelete(NULL);
}
基于STM32F407的FreeRTOS学习笔记(1)——环境搭建
其实从很早之前就想学实时操作系统(RTOS)了,但是一直没有时间去学,以前使用STM32单片机一直停留在逻辑开发以及前后台系统,而真正被广泛使用的则是RTOS。 前后台系统则是我们常用的,使用一个主循环+许多的调用函数这些构成了后系统,利用中断进行异常处理则是前系统,而RTOS则是将任务按照优先级排列,优先进行优先级高的任务例如单片机内如的中断服务函数。 操作更加的灵活方便,也是嵌入式软件工程师必备的技能之一,而FreeRTOS顾名思义,开源且免费,是我们小白入门的不二之选。安装FreeRTOS 某度上搜索FreeRTOS,点入FreeRTOS的官网,进入下载第一个安装包(包含源码) 下载好源码后,我们利用CUBEMX创建我们的工程,我选择的是STM32F407ZGT6这块芯片,之后将我们的下载的RTOS中的Source文件夹拷贝到我们利用CUBEMX创建的工程目录中。 其中的portable文件夹中只需保留以下文件即可,在工程中,我们将这些文件夹的内容全部添加到工程内( MemMang中的文件是实施方法,众多的heap文件我们只需要添加一个即可 ),并从之前FreeRTOS的源码中找到Demo文件夹,找到我们对应的单片机的文件夹。将FreeRTOSConfig.h文件即配置文件拷贝入我们的工程目录,这样子即可完成工程模板的创建。 之后我们编译工程,会发现有关于系统时钟的报错,没有SystemCoreClock的定义。 我们在FreeRTOSConifg.h文件中找到44行,发现这个定义是并不是在MDK这个平台使用的,我们将其进行修改,使之在MDK平台适用 我们去这些文件中挨个注释掉这些重复的函数。(我上述是添加了好几个heap文件,实际上添加一个heap文件即可) 之后我们又遇到了几个报错,这些报错主要是在FreeRTOSConfig.h中我们使能了几个构造函数,但是并没有定义这些构造函数,因此我们需要在FreeRTOSConfig.h中关闭这些构造函数 将对应的构造函数值改为0即可关闭这些构造函数。 这样子我们的编译就没问题啦
基于STM32F407的FreeRTOS学习笔记(3)——任务的挂起与恢复
上一期学习了任务的创建和删除,这一期学习任务的挂起与恢复。 所谓的挂起,也可以认为是 暂停 ,将运行中的任务挂起后,任务将暂停运行,直至系统恢复任务的运行。 在FreeRTOS的API文档中找到任务挂起函数的介绍,函数需要的参数为我们想要挂起的任务句柄,如果传递为NULL则暂停我们的调用任务。 同样的,在文档中也可以找到恢复任务函数介绍。 接下来我们实现一个任务,目标是当LED1闪烁5次后挂起LED0闪烁的任务,当LED1再闪烁5次后恢复LED0闪烁的任务。 我们在API中找到查询任务状态的函数eTaskGetState,该函数传入参数为任务句柄,返回参数为任务状态。 LED0在进行vTaskDelay时是处于阻塞态,因此我们只需要判断LED0是阻塞状态还是挂起状态,再进行挂起和恢复操作。 因此我们的代码如下void LED_TOG2(void * pvParameters)//参数为 void * pvParameters
{
while(1)
{
printf("LED_TOG2 running\r\n");//串口打印运行信息
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_9);//LED0翻转
LED2_Number++;//LED0翻转计数
if(LED2_Number %10==0)
{
if(eTaskGetState(LED_TOG_Handler) == eSuspended)
{
vTaskResume(LED_TOG_Handler);//LED0任务恢复
}
if(eTaskGetState(LED_TOG_Handler) == eBlocked )
{
vTaskSuspend(LED_TOG_Handler);//LED0任务挂起
}
}
vTaskDelay(500);//延迟500ms
}
}
#FreeRTOS#
基于STM32F407的FreeRTOS学习笔记(5)——消息队列(任务间通信与同步)
前言在数据结构中有一种很重要的数据结构叫做队列,其特点是数据先进先出。在FreeRTOS中也有一类队列,我们利用这类队列在FreeRTOS中实现任务与任务间的消息传递,所以也可以称之为消息队列。 队列是任务间通信的主要形式。它们可以用于在任务之间以及中断和任务之间发送消息。在大多数情况下,它们作为线程安全的 FIFO(先进先出)缓冲区使用,新数据被发送到队列的后面, 尽管数据也可以发送到前面。(拷贝自FreeRTOS开发者文档) 队列通过这样子的结构在任务间单方向传递消息。 在FreeRTOS的API引用文档中我们可以看到队列的控制函数。 我们在文档中找到队列创建函数(动态) 首先我们需要在程序中包含入queue.h文件才能使用队列。 其次和之前几期的操作一样,我们需要在FreeRTOSConfig.h文件中需要配置相对应的宏以激活该创建队列的构造函数。 xQueueCreate的参数有两个,首先是uxQueueLength队列可同时容纳的最大项目数简而言之也就是:这个队列有多长。 其次是uxItemSize,顾名思义是每一个项目(每个小块块)能存储多少数据(字节) 最后要强调的是,这个函数的返回值是QueueHandle_t,即以句柄的形式返回,因此我们创建任务的时候也需要以句柄变量接收其返回值。代码测试我们在启动函数中加入我们创建队列的函数,其长度为1,每个项目的大小为一个字节。 我们接着在API引用文档中找到关于队列发送的函数(如上)。 发送的函数平平无奇,但是有几点需要注意; 首先是发布项目按副本排队而不是引用指针,指的是我们传入的数据是先拷贝的临时变量传入,而并非我们传入数据的地址,我想这样子是为了避免在接收端时对数据进行修改导致错误。 其次是该函数不能在中断函数中调用(有专门的函数是在中断中发布项目的) 函数参数中的第三项xTicksWait简单的理解就是可等待的最大时间,我们如果我们的队列已满则尝试等待,超过一定周期认为超时则项目发布失败。 我们在按钮检测任务中编写:按下按钮2时向队列中放入字符p(p初始值为‘a’),每按下这个按钮,p的值递增。 还有一个按钮3,按下按钮3则在队列中读取一则消息,并打印出来。void Get_info(void * pvParameters)
{
unsigned char p = 'a';
unsigned char r;
while(1)
{
unsigned char key = KEY_Scan(0);
BaseType_t err;
if(key==1)
{
memset(informationbuff,0,400);
vTaskGetRunTimeStats(informationbuff);
printf("%s\r\n",informationbuff);
}
if(key==2)
{
printf("Key_2 Press\r\n");
if(KeyNumberHandler!=NULL)//队列句柄有效
{
err = xQueueSend(KeyNumberHandler,&p,10);
p++;
if(err!=pdTRUE)
{
printf("Send Fail \r\n");
}
else
{
printf("Send %c Success\r\n",p);
}
}
}
if(key==3)
{
printf("Key 3 Press\r\n");
if(KeyNumberHandler!=NULL)//队列句柄有效
{
xQueueReceive(KeyNumberHandler,&r,10);
printf("Queue Receive:%c \r\n",r);
r = '\0';//清空
}
}
vTaskDelay(10);
}
}
我们在按钮检测任务中加入按钮2和按钮3的情况,并且定义了一个变量err来检测我们的队列是否添加成功,我们观察串口并看看打印情况。 可以看到,我们按下按钮2,成功将 ‘b’ 消息送入队列(我们是先p++再送入队列的) 我们再继续按下按钮2,由于队列的长度为1,且队列的项目并没有出队列,因此串口会提示送入队列失败。 此时p等于 ' c ',我们按下按钮3,让数据出队列,并且再按下一次按钮3读取是否有数据。 可以看到,读取队列之后队列的内容将被释放,后续的内容将前进。之后我们再按下按钮2 ,此时就可以向队列中送入数据(我多按了一下)。 除此之外,FreeRTOS中还有一个函数为xQueueOverwrite,传入参数除了没有阻塞时间之外和xQueueSend一样,它的作用为将消息送入队列,如果没有空间则覆盖最后一个空间,我们将按钮2中的函数换为该函数再试试。 ##freeRTOS# 我们每次送入队列都成功,因为它会把队列的最后一个项目覆盖掉。 关于队列的介绍就到此啦,具体的API可以上FreeRTOS的官网查看参考文档。
基于STM32的甲醛测量仪以及了解盘中孔工艺
这段时间很久没更新了哈,这几天在完善我的甲醛测量仪。其实对于多功能测量仪来说也只是半成品,因为很多东西我还没做上去包括光照测量(芯片和模块都忘记买了)还有红外测量等等系列。 目前的完成度是:TVOC/CH2O/CO2这些气体的测量,温度(NTC电阻测量),简单的GUI设计,锂电池和TypeC混合供电系统。 当然啦,这个只是其中完成的一部分,实际上可以进行很多拓展内容,拟拓展如下: 外接MAX30100模块可以测量心率以及血氧数据,这个模块也是之前经常使用的模块,可以选择外接接口也是方便选择使用。 MXL90614红外测温模块,使用I2C协议通讯,较为方便准确的获取环境温度和红外温度。 SHT30温湿度传感器,可能主要的目的是用来替代NTC电阻获取温度以及获取湿度数据,当然他也是利用I2C进行通讯的,可能到时候会选择集成到电路上。 除此之外也会选择加上一些实时时钟以及试图使用ESP32来作为主控或者使用ESP32来辅助云端通讯。不过这些都是后话。盘中孔 因为需要对其做许多拓展,之后电路可能会较为麻烦,所以针对甲醛测量仪我设计了一个6层的电路板。没有选择4层是因为在之后升级迭代过程中,可以直接在6层板的基础上更新,减少之后需要重新布线的烦恼。 当然设计6层板的目的不仅仅是为了更方便布线,更可以进一步的缩小PCB体积。相比于传统的4层板,在紧凑型电子电路中,6层板拥有更小的干扰和更优越的性能。 在设计的时候由于大部分都是贴片设计,不可避免的需要使用过孔进行布线,实现不同电气层的跨越,但是之前就有网友指出了我在布线中的不规范行为,在焊盘上直接打孔。 这会导致在焊接过程中,锡会从焊盘上的孔中直接流下导致脱焊、虚焊。这种情况就可能会造成电路出现问题,而且很难察觉,那么如何去解决这个问题呢?有些朋友建议我去了解一下盘中孔工艺。普通的过孔 之后我去查阅了一些盘中孔工艺的相关资料,大概就是在打过孔的时候利用树脂填满过孔,再用铜电镀封孔,这样子锡就不会从孔中流下去导致虚焊,表面也看不出痕迹。 同时在盘中打孔可以大幅度的减少PCB布局面积,在复杂的贴片芯片中可以很大程度上的方便布线的同时避免因为锡的流失而导致虚焊。 了解完工艺之后就准备打板、就近先看嘉立创下单小助手,居然有这项工艺、还免费!二话不说就先下单了。 当我收到六层板仔细查看的时候,可以看到使用盘中孔确实是会将过孔封住填平,并且免费2u沉金工艺的PCB真的很帅,嘉立创很赞! 所以大家在硬件设计过程中也需要去了解一下设计规范以及一些特殊工艺,防止在使用的过程中遇到很多问题而没有解决方向。 关于这个甲醛测量仪,由于没有专业的设备进行校准,因此获得的其实是一个参考值,具体精确的测量可能需要专业的设备来整定,不过这个设计之后还是会趋向集成化以及在此基础上堆上更多的功能。其实这个锂电池混动供电之前的电路不知道为什么老是出毛病,但是这个也是一次成功,有朋友留言说这种供电方式会导致测量电池电压的时候很不准确,关于这个现象我之后去仔细研究一下如何解决。 还有就是选择的电池似乎有点太大了,我开机了一晚上都还是正常工作,下次选择电池容量小一点的,这么大也没有必要。#嘉立创6层板##嘉立创PCB# #高多层PCB#