算命购物网站建设,大型企业名录查询,建设通属于官方网站,做软件项目需不需要有网站二进制信号量
二进制信号量#xff0c;又叫二值信号量#xff0c;要么是0#xff0c;要么是1#xff0c;也是通过Take和Give方式获取和释放#xff0c;用于控制对共享资源的访问。在每次访问共享资源之前需要获取二进制信号量#xff0c;若已被获取则任务会被阻塞直到二…二进制信号量
二进制信号量又叫二值信号量要么是0要么是1也是通过Take和Give方式获取和释放用于控制对共享资源的访问。在每次访问共享资源之前需要获取二进制信号量若已被获取则任务会被阻塞直到二进制信号量可用。不同于互斥信号量二进制信号量可以通过多次获取而被同一个任务持有即可用于同一任务对多个共享资源的排他性访问。 二进制信号量和互斥信号量有很大差别具体表现如下
使用场景互斥信号量Mutex通常用于多线程环境中的临界区访问控制以确保每次只有一个线程可以访问这个临界区。它的初始值为1可以通过 xSemaphoreTake() 和 xSemaphoreGive() 函数来获取和释放。* 二进制信号量Binary Semaphore初始值也为1, 但是它通常被用于线程同步即用于线程之间的通信表示某个线程执行完毕另外一个线程才能开始执行。它可以通过 xSemaphoreTake() 和 xSemaphoreGive() 函数来获取和释放。特性互斥信号量可以防止多个线程同时访问同一个共享资源从而避免竞态条件的出现。当一个线程占用了互斥信号量其他线程必须等待该线程释放信号量后才能执行。因此互斥信号量适合用于单个资源的访问控制。* 二进制信号量适合用于线程同步通过等待或发送信号量不同线程之间可以协调工作避免竞争和冲突的发生。例如一个线程在完成某个操作后可以通过发送信号量来通知另一个线程执行相应的操作。实现方式互斥信号量通常基于二进制信号量实现由于它只有一个计数器因此当一个线程请求互斥信号量时如果该信号量已被占用则该线程将被阻塞。待互斥信号量被释放后下一个请求该信号量的线程将得到通知并获得该信号量。* 二进制信号量是一种抽象的概念可以使用多种方法进行实现比如锁、信号、事件等。
二进制信号量的应用
前面讲过二进制信号量用于在两个任务间传递数据也就是我们可以在一个任务中释放信号量另一个任务中获取信号量。这就与互斥信号量有着本质的区别互斥信号量的获取和释放必须在同一任务中进行跨任务就会出错。 基于二进制信号量这种特性我们首先能想到应用的就是开关本节课的例程就是使用按键开关和LED的互动按动一次开关LED灯打开再按动一次LED关闭。
代码共享位置https://wokwi.com/projects/362766325741473793
#define KEY_PIN 20
#define LED_PIN 14
SemaphoreHandle_t led NULL; // 二进制信号量
void led_task(void *param_t){pinMode(LED_PIN, OUTPUT);while(1){if(xSemaphoreTake(led, 1000) pdTRUE){ //pdTRUE 和 pdPASS 值是相同的用哪个都可以digitalWrite(LED_PIN,!digitalRead(LED_PIN));vTaskDelay(200);}}
}
// 按键监控
void key_task(void *param_t){pinMode(KEY_PIN, INPUT_PULLUP); // 设置为带输出的上拉while(1){if(digitalRead(KEY_PIN)LOW){// 按键按下了xSemaphoreGive(led);vTaskDelay(200); // 等待去抖这里不用换算是为了节省时间我们不需要精确延时}}
}
void setup() {Serial.begin(115200);led xSemaphoreCreateBinary(); //创建二进制信号量xTaskCreate(key_task, KEY-MON, 1024, NULL, 1, NULL);xTaskCreate(led_task, LED-DSP, 1024, NULL, 1, NULL);
}
void loop() {delay(10);
}二进制信号量同样使用 SemaphoreHandle_t 对象存放句柄Mutex通过 xSemaphoreCreateMutex 函数创建 而二进制信号量则通过 xSemaphoreCreateBinary 创建如果不对二进制信号量进行初始化而直接使用的话会报内存溢出的错误。 Mutex创建后初值非零所以可以直接Take到而二进制信号量创建后初值为 0所有不能直接Take而是需要通过 xSemaphoreGive 先放入然后才可以通过 xSemaphoreTake 进行获取其他的操作与Mutex相同。
程序 key_task 现成模拟的是对按键开关的扫描首先对微动按键开关所在的引脚进行初始化因为需要读取开关的值所以我们用的是上拉输出INPUT_PULLUP模式而次引脚默认情况下可以读到的是高电平HIGH因为开关的另一边电路与GND相连所以当按下开关是接地导通读出的值是低LOW如果读出的电流是低那表示开关被按下了 通过xSemaphoreGive(led) 改变二值信号量的值。 另一个任务 led_task 则通过 xSemaphoreTake 一直等待信号的到达当信号到达后出发LED引脚的电平翻转实现LED亮灭的目的。
微动按键开关
微动按键开关是硬件设计中比较常用的元件一般有四个引脚横向观看引脚分为上下两组每组都是相连的而开关在正常情况下上下是不连通的只有当按下开关的时候才会接通。
A插脚 B基座 C弹片 D按钮 E盖板 理想环境下当我们按下开关时引脚接收高电平抬起时引脚继续回复低电平但打脸来的是那么得快因为开关属于机械零件在按下和抬起的瞬间会内部的弹片会产生震动这个阶段如果我们用示波器测量发现收到的波形并不是一个严格意义上的方波而是在按下和抬起的前后出现了轻微的抖动。 用示波器测试波形
所以我们要在第一次判断到按键电平变化时这里我们采集的是低电平有时候采集的是高电平首先要有一段时间的延迟这段延迟大概在1050ms之间延迟后再进行二次采集而当第二次产生电平变动时有可能是开关的释放这时候我们收到第二次电平变化后仍然采取一个1050ms的延时确保开关是真的被放开了所以我们在写代码的时候应该是
if(digitalRead(KEY_PIN)LOW){delay(30);if(digitalRead(KEY_PIN)LOW){// 触发按键响应}
}而我们模拟器不存在抖动的情况我们也就不需要做这一步了但需要注意的是 xSemaphoreGive 函数会立即返回如果不加下面的延时程序会一直发送信号而另一个线程接收信号后也没有延迟这就造成了多次触发所以我们在释放和获取之后都有一个200的延迟这里的200不是严格意义上的ms而是200个Tick因为我们没有用转换函数。
为什么不能换成全局变量
代码共享位置https://wokwi.com/projects/362790977921224705
#define KEY_PIN 20
#define LED_PIN 14virtual bool key_down false; // 是否按下了按键
void led_task(void *param_t){pinMode(LED_PIN, OUTPUT);while(1){if(key_down){ //pdTRUE 和 pdPASS 值是相同的用哪个都可以digitalWrite(LED_PIN,!digitalRead(LED_PIN));vTaskDelay(200);key_down false;}vTaskDelay(100); // 一定要让出CPU否则会一直在这里循环}
}
// 按键监控
void key_task(void *param_t){pinMode(KEY_PIN, INPUT_PULLUP); // 设置为带输出的上拉while(1){if(digitalRead(KEY_PIN)LOW){// 按键按下了key_down true;vTaskDelay(200); // 等待去抖这里不用换算是为了节省时间我们不需要精确延时}vTaskDelay(100); // 一定要让出CPU否则会一直在这里循环}
}
void setup() {Serial.begin(115200);xTaskCreate(key_task, KEY-MON, 1024, NULL, 1, NULL);xTaskCreate(led_task, LED-DSP, 1024, NULL, 1, NULL);
}
void loop() {delay(10);
}这样做理论上也是可以实现的如果只是做一个开关的话也是可以做到点灯的。 但问题就在于原来我们在LED任务中我妈是通过 xSemaphoreTake 实现等待的而此时CPU已经让出给其他线程我们的CPU利用率是很低的但在上述例程中采用了轮训的方式每间隔一段时间就扫描一次按键是否被按下了这种效率是极低的在多任务情况下轮训线程占用了CPU其他程序就得让路这就造成了不必要的资源浪费所以我们在开发过程中尽量使用二值信号量来代替线程间的通讯减少资源消耗。
同样在第一个例子中key的扫描我们也用到了轮训这无疑也会造成资源的浪费所以我们还可以对第一个二进制信号量的程序进行修改把key的轮训改为中断方式。
初认中断
在第一课讲到程序执行的时候我们有个图提到了中断。
中断Interrupt指的是计算机执行程序时由于硬件的某些信号或者软件的需要导致CPU中止正在执行的程序转而处理另一个事件或者程序的机制。中断可以使得CPU在不同任务之间快速地切换提高计算机的并发性和响应能力。 中断的触发条件通常包括硬件中断和软件中断两种情况
硬件中断是指由外部设备发出的需要及时处理的信号比如输入输出设备的请求、定时器、时钟等。当这些信号被检测到后CPU会在当前运行任务的中断点处保存当前状态并跳转到中断处理程序去执行。软件中断是指在程序中特意插入的一段代码用于实现某个具体功能或者服务。软件中断也可以被看作是一种人为中断例如系统调用、软件异常或者进程间通信等。
我们这里使用的是按键中断也就是硬终端其中的一种当引脚的电平发生变化的时候就会触发触发中断后不管CPU当前在干什么只要不是处于优先级更高的其他中断都会跳到中断服务函数中执行。 这里我们试试简单用到了中断后续会有专门的可成详细讲解同学么在此只需要简单了解即可。
代码共享位置https://wokwi.com/projects/362768211562473473
#define KEY_PIN 20
#define LED_PIN 14
SemaphoreHandle_t led NULL; // 二进制信号量
volatile TickType_t keyDeounce 0; // 按下按钮的时间
void led_task(void *param_t){pinMode(LED_PIN, OUTPUT);while(1){// 这种去抖方式是很Low的正确的方式要使用定时器。if((xSemaphoreTake(led, 1000) pdTRUE) ((xTaskGetTickCount() - keyDeounce) 200)){digitalWrite(LED_PIN,!digitalRead(LED_PIN));vTaskDelay(500);}}
}
// 中断服务函数
void IRAM_ATTR ISR() {keyDeounce xTaskGetTickCountFromISR(); // 记录下按下的时间用于放抖动正式开发中不要这样写有BugxSemaphoreGiveFromISR(led, NULL);
}
void setup() {Serial.begin(115200);led xSemaphoreCreateBinary(); //创建二进制信号量xTaskCreate(led_task, LED-DSP, 1024, NULL, 1, NULL);// 安装中断pinMode(KEY_PIN, INPUT_PULLUP);attachInterrupt(KEY_PIN, ISR, FALLING);
}
void loop() {delay(10);
}在setup函数中通过 attachInterrupt(KEY_PIN, ISR, FALLING) 安装了中断服务函数 ISRFALLING是下降沿触发另外还有上升沿、跳变沿等方式。 在安装中断服务函数之前需要将引脚设置为带上拉的输入以方便读取电平状态。 在Arduino中中断服务函数要通过 IRAM_ATTR 进行定义。 另外就是中断中使用的很多FreeRTOS函数和外面的不同都加有FromISR的后缀具体等到中断章节再细讲。 中断服务函数中首先记录了触发中断按键的时间用于比较然后通过 xSemaphoreGiveFromISR 方式释放了一个二进制信号量这与在任务中释放函数有所不同。 在LED点灯的任务中首先判断信号量是否被释放了放抖动用。 其他的和原函数相同。 这样改外之后CPU使用率瞬间降下来了给其他可能存在的任务留下了很大的资源空间。
计数器信号量
FreeRTOS的信号量还剩最后一种叫做计数器信号量。 计数器信号量可以看作是一个内部维护计数的信号量当计数值为0时表示当前没有可用的信号量而当计数值大于0时则表示还有可用的信号量。每个任务在使用共享资源之前都需要获取信号量许可当信号量计数器为正时任务可以得到许可并访问共享资源同时信号量的计数器会减1。当任务释放共享资源时可以通过给信号量计数器加上一个值来释放许可。如果信号量计数器为0所有试图获取许可的任务都将被阻塞等待计数器变成非0值。 相对于二进制信号量计数器信号量可以允许多个任务同时访问同一共享资源并且支持多对多的任务访问模式。因此在实际应用中计数器信号量更适合那些需要控制访问数量的场景。
计数器信号量应用的场景
计数器信号量Counting Semaphore主要用于多个任务之间同步和控制访问共享资源的场景。下面列举了一些计数器信号量常见的应用场景
任务同步当多个任务需要在某个时刻完成某项任务时可以使用计数器信号量来控制任务的执行流程确保任务按预期顺序执行。缓冲区管理当多个任务需要访问同一个缓冲区时可以使用计数器信号量来控制缓冲区的访问数量避免出现竞争条件。系统资源分配当多个任务需要访问同一个系统资源如堆、队列等时可以利用计数器信号量来确保系统资源的安全性和有效性。输入/输出控制当多个任务需要共享输入/输出设备时可以使用计数器信号量来控制设备的访问数量同时避免出现数据竞争和冲突。动态优先级调度当多个任务需要实时响应某种事件时可以基于计数器信号量实现动态优先级调度机制以确保系统的响应速度和稳定性。
需要注意的是计数器信号量需要合理设置初始值和计数步长以适应不同的应用场景和需求。在实际应用中需要根据具体情况进行合理的调整和优化。
缓冲区管理我们之前用的互斥信号量可以完全代替输入输出控制上一个例程中开关灯的例子我们用了二进制信号来那个也实现了任务同步和动态优先级调度在下面章节关于时间标志组我们会讲到排除这些计数器信号量最重要的应用场景就是系统资源分配。
有这样一个例子我们班一共有20台示波器但我们一共有52个同学如果我们都要使用示波器的时候测量按键抖动这时候我们52个同学应该如何分配呢只能排队先到先得但用完后要还回去让给其他同学用如果你在需要用还得排队等。 这时候我们就可以创建一个计数器信号量最大值是20表示我们一共有20个示波器的资源初始值也是20表示我们有20个闲置资源也就是20台示波器。 有同学需要借走的时候就使用 xSemaphoreTake 获取返回pdPASS或者pdTRUE表示后去成功用完后依然要通过 xSemaphoreGive 还setup阶段通过 xSemaphoreCreateCounting(CAPACITY,FOOD) 构造了一个容积是100初始值是0的计数器信号量并通过预设制造了一些厨师和一些吃货。 回来。 对应到物联网开发中这个资源可能是一台设备上的多个网卡、多个USB、多个串口、IIC、SPI等外设也可以是多个缓冲区等内部资源。 这就是计数器信号量的一种用法。
还有一种用法是生产者和消费者的关系。 拿回我们上几节课讲到的吃货和厨子的例子。 假设这次餐厅中一开始并没有汉堡可以吃来了许多个吃货都等待着厨子做汉堡我们的厨子可以是一个也可以是多个这样就形成了多对多的关系。 冰箱的容积是100意味着厨子最多可以做100个汉堡如果做多了就放不进去了吃货们当把冰箱里食物吃完的时候就需要等待。 这时候我们初始化计数器信号量的时候就要告诉句柄最大容积是100当前是0然后厨子们通过 xSemaphoreTake 做汉堡吃货们通过 xSemaphoreGive 吃汉堡。 对应物联网的开发中可以用一个分布式计算的例子理解 生产者产生需要计算的数据并放入到队列中消费者从队列中读取数据并进行计算计算完毕后再拿第二组数据循环往复。 产生数据的生产者可能存在多个而计算数据的消费者也可能存在多个但大家对同一个计数单元进行操作。当然这种方式后面我们也会用到消息队列的方式解决
计数器信号量例程
代码共享位置https://wokwi.com/projects/362794364621551617
volatile int16_t quantity 100; // 食物的剩余数量
volatile int16_t eatenCount0; // 总共吃掉的实物数量
SemaphoreHandle_t hamburg NULL; // 汉堡计数器信号量句柄
volatile uint8_t foodie_num0; // 吃货计数器
volatile uint8_t chef_num0; // 厨子计数器
// 吃货线程
void foodie_task(void *param_t){int16_t eaten 0; // 吃掉的食物累计uint8_t my_num foodie_num;while(1){if(xSemaphoreTake(hamburg, 1000) pdPASS){eaten;printf([吃货] %d 号吃货吃了一个汉堡我一共吃了%d 个。\n, my_num, eaten);}else{printf([吃货] %d 号吃货没有等到汉堡\n, my_num);}vTaskDelay(pdMS_TO_TICKS(random(500,2000)));}
}
// 厨师线程
void chef_task(void *param_t){uint8_t my_num chef_num;while(1){if(xSemaphoreGive(hamburg) pdTRUE){printf([厨子] %d 号厨子生产了一个汉堡冰箱里一共有%d个汉堡。\n, my_num, uxSemaphoreGetCount(hamburg));}else{printf([厨子] %d厨子冰箱已满无法制作汉堡\n, my_num);}vTaskDelay(pdMS_TO_TICKS(random(100,1000)));}
}
#define FOODIE_COUNT 3 // 吃货的总数量
#define CHEF_COUNT 1 // 厨子的总数量
#define CAPACITY 100 // 冰箱的容量
#define FOOD 0 // 冰箱内初始食物的数量
void setup() {Serial.begin(115200);hamburg xSemaphoreCreateCounting(CAPACITY,FOOD); // 创建一个计数器信号量容量是100初始值是0for(int i0; iFOODIE_COUNT; i){xTaskCreate(foodie_task, Foodie, 1024*4, NULL, 1, NULL);}for(int i0; iCHEF_COUNT; i){xTaskCreate(chef_task, Chef, 1024*4, NULL, 1, NULL);}
}
void loop() {delay(10);
}例程中通过修改 FOODIE_COUNT、CHEF_COUNT来调厨师和吃货的数量也就是生产者和消费者的数量通过CAPACITY、FOOD来调整冰箱的大小和初始食物量也就是计数器的容积和初始值。 setup阶段通过 xSemaphoreCreateCounting(CAPACITY,FOOD) 构造了一个容积是100初始值是0的计数器信号量并通过预设制造了一些厨师和一些吃货。 chef_task 任务中厨师随机时间生产汉堡通过 xSemaphoreGive(hamburg) 把汉堡放入到冰箱里如果冰箱里还有空位值没有超过100则返回pdTRUE表示释放成功如果返回的是其他值则表示释放失败也就是计数器满了。 计数器当前数值可以通过 uxSemaphoreGetCount(hamburg) 查看。 foodie_task 任务中吃货通过 xSemaphoreTake(hamburg, 1000) 获得一个汉堡如果冰箱里现在没有汉堡计数器值为0可以等待一秒钟但一秒中后还没有获取到则表示获取失败。 这个例程通过厨师不断释放信号量厨子不断获取信号量的方式讲述了计数器信号量的应用。
关于信号量的所有API可以参考https://www.freertos.org/zh-cn-cmn-s/a00113.html