• [技术干货] C语言实例_奇偶校验算法
    一、奇偶校验算法奇偶校验算法(Parity Check Algorithm)是一种简单的错误检测方法,用于验证数据传输中是否发生了位错误。通过在数据中添加一个附加的奇偶位(即校验位),来实现错误的检测和纠正。在奇偶校验算法中,假设每个字节由8个比特(位)组成。奇偶校验位的值取决于数据字节中的1的个数。如果数据字节中1的个数是偶数个,奇偶校验位被设置为0;如果1的个数是奇数个,奇偶校验位被设置为1。这样,在接收端,通过统计接收到的数据字节中1的个数,就可以检测出位错误。具体的奇偶校验算法包括以下几个步骤:(1)发送端:在发送数据字节之前,统计数据字节中1的个数,根据个数设置奇偶校验位的值,并将数据字节和奇偶校验位一起发送。(2)接收端:在接收数据字节后,再次统计接收到的数据字节中1的个数,与接收到的奇偶校验位进行比较。如果两者不一致,说明数据传输中发生了位错误。奇偶校验算法在以下场景中常被使用:(1)串行通信:在串行通信中,奇偶校验算法可以用于检测数据传输过程中发生的位错误。发送端计算奇偶校验位并附加到发送的数据字节上,接收端通过验证奇偶校验位来判断接收到的数据是否正确。(2)存储介质:在一些存储介质上,如磁盘驱动器或闪存存储器,奇偶校验算法可以用于检测数据读取或写入过程中发生的位错误。在存储数据时,计算奇偶校验位并与数据一起存储;在读取数据时,再次计算校验位并与存储的校验位进行比较,以确保数据的完整性和准确性。(3)错误检测:奇偶校验算法也可以用于其他需要简单错误检测的场景。例如,在计算机内存或寄存器中,奇偶校验位可以用于检测存储数据过程中的位错误,以避免数据的错误使用或传输。奇偶校验算法只能检测到位错误,而不能纠正错误。如果检测到错误,则需要采取其他纠错措施或请求重新传输数据。二、代码实现场景:在单片机通信里,单片机需要向上位机发送数据。 下面代码演示两个函数,针对发送方和接收方使用,使用奇偶校验算法对数据进行验证。2.1 发送方函数void sender_send_data_with_parity(unsigned char* data, int length) { // 统计数据字节中1的个数 int count = 0; for (int i = 0; i < length; i++) { unsigned char byte = data[i]; for (int j = 0; j < 8; j++) { if ((byte >> j) & 1) { count++; } } }​ // 计算奇偶校验位,如果1的个数是偶数,则校验位为0,否则为1 unsigned char parity_bit = (count % 2 == 0) ? 0 : 1;​ // 发送数据字节和奇偶校验位 for (int i = 0; i < length; i++) { send_byte(data[i]); } send_byte(parity_bit);}2.2 接收方函数void receiver_receive_data_with_parity() { // 接收数据 unsigned char received_data[MAX_LENGTH]; int length = receive_data(received_data);​ // 统计接收到的数据字节中1的个数 int count = 0; for (int i = 0; i < length - 1; i++) { unsigned char byte = received_data[i]; for (int j = 0; j < 8; j++) { if ((byte >> j) & 1) { count++; } } }​ // 比较接收到的奇偶校验位与数据字节中1的个数是否一致 unsigned char expected_parity_bit = (count % 2 == 0) ? 0 : 1; unsigned char received_parity_bit = received_data[length - 1];​ if (expected_parity_bit != received_parity_bit) { // 发生了位错误 handle_error(); } else { // 数据传输正常 process_data(received_data, length - 1); }}
  • [问题求助] ffmpeg 视频剪切问题
    现在想用ffmpeg剪切视频,如何能精确定位时间呀? 每次剪切总是会有2秒以上的误差。 剪切是靠时间还是靠视频帧呢?
  • [技术干货] C语言实例_CRC校验算法
    一、CRC介绍CRC(Cyclic Redundancy Check,循环冗余校验)是一种常用的错误检测技术,用于验证数据在传输或存储过程中是否发生了错误。它通过对数据进行一系列计算和比较,生成一个校验值,并将其附加到数据中。接收方可以使用相同的算法对接收到的数据进行校验,然后与接收到的校验值进行比较,从而确定数据是否存在错误。CRC校验通常用于以下方面:(1)数据传输的可靠性:在数据通过媒体或网络进行传输时,可能会发生噪声、干扰或其他传输错误。通过在数据中添加CRC校验值,接收方可以检测到传输过程中是否发生了错误,并采取相应措施,如请求重新发送数据。(2)存储介质的完整性检测:在存储介质上读取或写入数据时,可能会发生位翻转、介质故障等错误。通过在数据存储时使用CRC校验,可以在读取数据时检测到这些错误,并提供数据的完整性保证。(3)网络通信协议:许多网络通信协议(如Ethernet、WiFi、USB等)使用CRC校验作为数据帧的一部分,以确保传输的数据准确无误。接收方在接收到数据帧后,使用CRC校验来验证数据的完整性。在项目中,CRC校验广泛应用于各种通信系统、存储系统和数据传输系统中。通过使用CRC校验,可以提高数据的可靠性,并减少传输或存储过程中的错误。它可以检测到数据位级别的错误,并提供一定程度的数据完整性保证。CRC校验在保障数据可靠性和完整性方面具有重要作用,特别是在对数据完整性有较高要求的应用场景中。二、示例代码以下C语言代码演示如何获取一段数据的CRC校验值:#include <stdio.h>#include <stdint.h>​// CRC校验函数uint16_t crc16(uint8_t *data, int length){ uint16_t crc = 0xFFFF; for (int i = 0; i < length; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 1) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc;}​// 封装的CRC校验函数调用uint16_t calculateCRC(uint8_t *data, int length){ return crc16(data, length);}​int main(){ uint8_t message[] = {0x01, 0x02, 0x03, 0x04, 0x05}; int length = sizeof(message) / sizeof(message[0]); uint16_t crc = calculateCRC(message, length); printf("CRC: 0x%04X\n", crc); return 0;}在上面代码中,crc16 函数实现了CRC校验的计算逻辑。采用了常用的CRC-16算法(0xA001多项式)。calculateCRC 函数是对 crc16 的封装,用于调用CRC校验函数并返回校验结果。在 main 函数中,通过调用 calculateCRC 函数来计算给定数据的CRC校验值,并将结果打印输出。代码中的CRC校验函数和封装函数是基于无符号8位字节和无符号16位整数的数据类型进行计算的。三、案例:数据校验场景:在单片机通信里,单片机需要向上位机发送一段数据。比如,存放在char buff[1024];这个数组里。 需要封装两个函数,单片机端调用函数对这段数据进行CRC校验,封装校验值,然后上位机收到数据之后验证CRC,校验数据是否传输正确。3.1 发送方(封装校验值)#include <stdio.h>#include <stdint.h>​// CRC校验函数uint16_t crc16(uint8_t *data, int length){ uint16_t crc = 0xFFFF; for (int i = 0; i < length; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 1) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc;}​// 封装CRC校验值到数据中void appendCRC(uint8_t *data, int length){ uint16_t crc = crc16(data, length); data[length] = crc & 0xFF; // 将低8位放入数据末尾 data[length + 1] = crc >> 8; // 将高8位放入数据末尾的下一个位置}​int main(){ uint8_t buff[1024] = {0x01, 0x02, 0x03, 0x04, 0x05}; // 原始数据 int length = 5; // 数据长度 // 在原始数据后追加CRC校验值 appendCRC(buff, length); // 输出发送的数据(包括CRC校验值) printf("发送的数据:"); for (int i = 0; i < length + 2; i++) { printf("%02X ", buff[i]); } printf("\n"); return 0;}在发送方的代码中,使用 appendCRC 函数将CRC校验值追加到原始数据的末尾。3.2 接收方(校验数据)#include <stdio.h>#include <stdint.h>​// CRC校验函数uint16_t crc16(uint8_t *data, int length){ uint16_t crc = 0xFFFF; for (int i = 0; i < length; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 1) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc;}​// 验证CRC校验值是否正确int verifyCRC(uint8_t *data, int length){ uint16_t crc = crc16(data, length - 2); // 去除数据末尾的CRC校验值 // 获取接收到的CRC校验值 uint16_t receivedCRC = (data[length - 1] << 8) | data[length - 2]; // 比较计算得到的CRC校验值与接收到的CRC校验值 if (crc == receivedCRC) { return 1; // 校验通过 } else { return 0; // 校验失败 }}​int main(){ uint8_t receivedData[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0xC2, 0x45}; // 收到的数据(包括CRC校验值) int length = sizeof(receivedData) / sizeof(receivedData[0]); // 验证CRC校验值是否正确 int crcResult = verifyCRC(receivedData, length); if (crcResult) { printf("CRC校验通过\n"); // TODO: 进一步处理正确的数据 } else { printf("CRC校验失败\n"); // TODO: 处理校验失败的情况 } return 0;}在接收方的代码中,使用 verifyCRC 函数验证接收到的数据的CRC校验值是否正确。如果校验通过,可以执行进一步的数据处理操作;如果校验失败,可以进行异常处理。示例中的CRC校验函数是基于无符号8位字节和无符号16位整数的数据类型进行计算的。可以根据实际需求进行适当修改,以适应不同的数据类型和CRC算法。
  • [技术干货] Linux下C语言调用libcurl库获取天气预报信息
    一、概述当前文章介绍如何在Linux(Ubuntu)下使用C语言调用libcurl库获取天气预报的方法。通过HTTP GET请求访问百度天气API,并解析返回的JSON数据,可以获取指定城市未来7天的天气预报信息。二、设计思路【1】使用libcurl库进行HTTP GET请求在代码中包含<curl/curl.h>头文件,以便使用libcurl库使用curl_easy_init()函数初始化curl设置请求选项,包括URL、写回调函数和写数据参数使用curl_easy_perform()函数执行请求【2】编写回调函数,将响应数据存储在内存中定义一个结构体,包含存储响应数据的指针和长度在回调函数中将响应数据拷贝到内存中,并动态调整内存大小返回已拷贝的数据大小【3】解析JSON数据使用json_tokener_parse()函数解析返回的JSON数据使用json_object_object_get_ex()函数获取指定字段的值使用json_object_array_length()函数获取数组长度使用json_object_array_get_idx()函数获取数组中的元素使用json_object_get_string()函数获取字符串值【4】打印天气预报信息遍历获取到的天气预报数据,依次获取日期、天气和温度使用printf()函数打印每一天的天气预报信息三、关键代码以下是主要的代码片段:// 定义回调函数,用于将响应数据存储在内存中size_t write_callback(void *ptr, size_t size, size_t nmemb, void *stream) { // ...}​// 子函数,用于获取指定城市未来7天的天气预报int get_weather_forecast(const char *city) { // ...}​int main() { const char *city = "your_city_code"; int ret = get_weather_forecast(city); // ...}四、使用说明【1】替换API密钥和城市代码:在示例代码中,将your_ak和your_city_code替换为你自己的百度API密钥和城市代码。【2】编译代码:使用合适的C编译器,如gcc,编译代码。gcc -o download_program download_program.c -lcurl【3】运行代码:在终端中运行生成的可执行文件。./download_program【4】查看天气预报:程序会打印出指定城市未来7天的天气预报信息。五、完整代码HTTP GET请求访问百度天气API,并解析返回的JSON数据获取需要的天气信息。#include <stdio.h>#include <stdlib.h>#include <string.h>#include <curl/curl.h>#include <json-c/json.h>​// 定义回调函数,用于将响应数据存储在内存中size_t write_callback(void *ptr, size_t size, size_t nmemb, void *stream) { size_t realsize = size * nmemb; struct string *mem = (struct string *)stream;​ mem->ptr = realloc(mem->ptr, mem->len + realsize + 1); if (mem->ptr == NULL) { fprintf(stderr, "内存分配失败\n"); return 0; }​ memcpy(&(mem->ptr[mem->len]), ptr, realsize); mem->len += realsize; mem->ptr[mem->len] = '\0';​ return realsize;}​// 子函数,用于获取指定城市未来7天的天气预报int get_weather_forecast(const char *city) { char url[256]; sprintf(url, "https://api.map.baidu.com/weather/v1/?district_id=%s&ak=your_ak", city);​ CURL *curl = curl_easy_init(); struct string response; response.ptr = malloc(1); response.len = 0;​ if (curl && response.ptr) { // 设置请求选项 curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);​ // 执行请求 CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "请求失败: %s\n", curl_easy_strerror(res)); free(response.ptr); curl_easy_cleanup(curl); return -1; }​ // 解析JSON数据 struct json_object *json = json_tokener_parse(response.ptr); if (json == NULL) { fprintf(stderr, "JSON解析失败\n"); free(response.ptr); curl_easy_cleanup(curl); return -1; }​ // 解析天气预报 struct json_object *result, *weather_data; json_object_object_get_ex(json, "result", &result); json_object_object_get_ex(result, "weather_data", &weather_data);​ int i; int num_days = json_object_array_length(weather_data); for (i = 0; i < num_days; i++) { struct json_object *day = json_object_array_get_idx(weather_data, i); const char *date, *weather, *temperature; date = json_object_get_string(json_object_object_get(day, "date")); weather = json_object_get_string(json_object_object_get(day, "weather")); temperature = json_object_get_string(json_object_object_get(day, "temperature"));​ printf("日期:%s\n天气:%s\n温度:%s\n\n", date, weather, temperature); }​ free(response.ptr); json_object_put(json); } else { fprintf(stderr, "初始化失败\n"); if (response.ptr) { free(response.ptr); } if (curl) { curl_easy_cleanup(curl); } return -1; }​ curl_easy_cleanup(curl); return 0;}​int main() { const char *city = "your_city_code"; int ret = get_weather_forecast(city); if (ret == 0) { printf("天气预报获取成功!\n"); } else { printf("天气预报获取失败!\n"); }​ return 0;}在示例代码中,使用curl_easy_setopt函数设置HTTP GET请求的URL,并通过CURLOPT_WRITEFUNCTION和CURLOPT_WRITEDATA选项指定回调函数,将响应数据存储在内存中。然后,使用json_tokener_parse函数解析返回的JSON数据,并提取其中的天气预报信息。通过json_object_object_get和json_object_array_get_idx等函数获取JSON对象和数组中的数据。注意:代码中的URL中的YOUR_AK和YOUR_CITY_CODE需要使用你自己的百度API密钥和城市代码替换。通过调用get_weather_forecast函数,可以获取指定城市未来7天的天气预报并打印出来。
  • [技术干货] Linux下C语言调用libcurl库下载文件到本地
    一、项目介绍当前文章介绍如何使用C语言调用libcurl库在Linux(Ubuntu)操作系统下实现网络文件下载功能。libcurl是一个开源的跨平台网络传输库,用于在C和C++等编程语言中实现各种网络通信协议的客户端功能。它支持多种协议,包括HTTP、HTTPS、FTP、SMTP、POP3等,可以方便地进行数据的上传和下载操作。以下是libcurl库的一些主要特点和功能:1. 跨平台性:libcurl库可以在多个操作系统上使用,包括Windows、Linux、macOS等。这使得开发者可以轻松地编写跨平台的网络应用程序。2. 多协议支持:libcurl支持多种网络协议,包括HTTP、HTTPS、FTP、SMTP、POP3等。它提供了丰富的API,使得开发者可以通过简单的接口调用来实现与远程服务器之间的通信。3. 断点续传:libcurl支持断点续传功能,即可以从已经下载的位置继续下载文件。这对于大文件的下载非常有用,可以节省带宽和时间,并避免重新下载整个文件。4. SSL/TLS支持:libcurl可以通过OpenSSL或其他TLS/SSL库来进行安全传输。它支持HTTPS协议,并提供了SSL证书验证、加密和解密等功能,以确保数据的安全性。5. 异步和多线程支持:libcurl提供了异步和多线程操作的支持,可以在网络传输过程中进行其他任务处理,提高程序的并发性和性能。6. 适应性和灵活性:libcurl库提供了丰富的选项和回调函数,允许开发者根据自己的需求进行定制和扩展。开发者可以配置代理服务器、设置超时时间、自定义HTTP头部等。7. 良好的错误处理和调试支持:libcurl提供了详细的错误代码和错误信息,方便开发者进行错误处理和故障排除。它还提供了调试输出功能,可打印详细的网络通信和传输信息。8. 并发连接管理:libcurl支持并发连接管理,可以同时处理多个网络请求。这对于高并发的网络应用非常有用,可以提高系统的吞吐量和性能。二、环境准备libcurl库: 可以通过在终端中运行以下命令进行安装:sudo apt-get install libcurl4-openssl-devGitHub仓库:cid:link_0 libcurl官网: cid:link_1三、设计步骤3.1 引入头文件在C代码文件中,需要引入curl/curl.h头文件,以便使用libcurl库提供的函数和结构体。#include <stdio.h>#include <curl/curl.h>3.2 初始化libcurl在程序开始之前,需要初始化libcurl库。可以通过调用curl_global_init函数来完成。curl_global_init(CURL_GLOBAL_DEFAULT);3.3 设置下载选项接下来,需要设置下载选项,包括要下载的URL链接、保存到本地的文件路径等。可以使用curl_easy_setopt函数来设置选项。CURL *curl = curl_easy_init();if (curl) { curl_easy_setopt(curl, CURLOPT_URL, "http://example.com/file.zip"); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); // fp是文件指针,用于保存下载的数据}3.4 执行下载请求调用curl_easy_perform函数来执行下载请求,并将文件保存到指定路径。在执行过程中,libcurl库会自动处理网络传输和接收文件数据。CURLcode res = curl_easy_perform(curl);if (res != CURLE_OK) { fprintf(stderr, "下载失败: %s\n", curl_easy_strerror(res));}3.5 清理资源最后,在程序结束时,需要清理libcurl的资源。可以通过调用curl_easy_cleanup函数来完成。curl_easy_cleanup(curl);3.6 完整示例代码下面是一个完整的示例代码,演示如何使用C语言和libcurl库在Linux(Ubuntu)下实现网络文件下载功能:#include <stdio.h>#include <curl/curl.h>​int main() { CURL *curl = curl_easy_init(); FILE *fp = fopen("downloaded_file.zip", "wb"); //打开一个文件用于保存下载的数据​ if (curl && fp) { curl_easy_setopt(curl, CURLOPT_URL, "http://example.com/file.zip"); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);​ CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "下载失败: %s\n", curl_easy_strerror(res)); }​ fclose(fp); } else { fprintf(stderr, "初始化失败\n"); }​ curl_easy_cleanup(curl); curl_global_cleanup();​ return 0;}3.7 编译和运行在终端中,使用以下命令编译示例代码:gcc -o download_program download_program.c -lcurl然后,通过运行生成的可执行文件来执行下载程序:./download_program四、完整代码下面是一个封装了网络文件下载功能的子函数:#include <stdio.h>#include <curl/curl.h>// 定义回调函数,用于将下载的数据写入本地文件size_t write_callback(void *ptr, size_t size, size_t nmemb, void *stream) { return fwrite(ptr, size, nmemb, (FILE *)stream);}// 子函数,用于下载网络文件到本地int download_file(const char *url, const char *output_filename) { CURL *curl = curl_easy_init(); FILE *fp = fopen(output_filename, "wb"); // 打开一个文件用于保存下载的数据 if (curl && fp) { // 设置下载选项 curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); // 执行下载请求 CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "下载失败: %s\n", curl_easy_strerror(res)); fclose(fp); curl_easy_cleanup(curl); return -1; } fclose(fp); } else { fprintf(stderr, "初始化失败\n"); if (fp) { fclose(fp); } if (curl) { curl_easy_cleanup(curl); } return -1; } curl_easy_cleanup(curl); return 0;}int main() { const char *url = "http://example.com/file.zip"; const char *output_filename = "downloaded_file.zip"; int ret = download_file(url, output_filename); if (ret == 0) { printf("文件下载成功!\n"); } else { printf("文件下载失败!\n"); } return 0;}在上面的代码中,download_file函数实现了下载网络文件到本地的功能。将要下载的URL链接和保存到本地的文件路径作为函数参数传入。函数内部使用libcurl库设置下载选项,执行下载请求,并将数据写入本地文件。在main函数中,可以调用download_file函数来实现文件下载。通过判断函数返回值,可以判断文件下载是否成功。编译和运行代码的步骤与之前提供的步骤相同。通过调用download_file函数实现网络文件下载功能,可以方便地在其他代码中复用该功能,并进行错误处理和扩展。
  • [技术干货] 基于STM32设计的出租车计费系统
    一、项目介绍在城市交通中,出租车是一种常见的交通工具。为了方便乘客和司机之间的交易,出租车计费系统被广泛应用于出租车行业。系统能够自动计算乘客的费用,提供准确、方便的计费服务,并且能够记录乘客的行驶数据,方便后续查询和管理。传统的出租车计费方式是基于人工计算,司机根据里程和时间进行估算并告知乘客费用。然而,这种计费方式容易存在误差和争议,并且对司机和乘客都不够方便和透明。因此,出租车行业迫切需要一种更加准确、高效和可靠的计费系统。基于此背景,本项目设计和开发一种基于STM32微控制器的出租车计费系统,以替代传统的人工计费方式。该系统将利用STM32微控制器的强大处理能力和丰富的外设接口,集成各种功能模块,实现自动计算乘客费用、显示计费信息等功能。通过该出租车计费系统,乘客只需在上车时按下对应按钮,系统将自动开始计费,并在显示屏上实时显示行驶时间、里程和费用等信息。乘客还可以通过按键输入特殊情况,如堵车或夜间行驶,以便系统进行相应的额外计费。当乘客下车时,系统将自动停止计费,并显示最终费用。同时,系统还将记录乘客的行驶数据以备查询和管理。二、系统设计思路2.1 系统架构出租车计费系统的主要组成部分包括:STM32微控制器、LCD显示屏、按键、计时电路、收费器和外部存储器。整个系统的架构如下:STM32微控制器:采用STM32F103RCT6作为系统的控制核心,负责接收并处理来自各个模块的输入信号,并控制液晶显示屏上的信息显示和收费器的操作。LCD显示屏:采用1.44寸LCD显示屏,用于显示当前的计费信息,包括行驶时间、里程和费用等。按键:用于输入乘客上车和下车的时间以及其他特殊情况,如堵车、夜间行驶等。计时电路:用于准确地测量行驶时间。收费器:负责根据计费规则和实时数据计算乘客的费用。外部存储器:用于存储行驶数据和计费规则。2.2 系统功能出租车计费系统具有以下主要功能:实时计算行驶时间和里程。根据计费规则自动计算乘客费用。在LCD显示屏上显示当前的计费信息。支持特殊情况的额外计费,如堵车、夜间行驶等。存储行驶数据和计费规则以备查询和更新。三、代码设计3.1 LCD显示屏代码#include "stm32f10x.h"​// 定义LCD引脚连接#define LCD_RS_PIN GPIO_Pin_0#define LCD_RS_PORT GPIOA#define LCD_RW_PIN GPIO_Pin_1#define LCD_RW_PORT GPIOA#define LCD_E_PIN GPIO_Pin_2#define LCD_E_PORT GPIOA#define LCD_D4_PIN GPIO_Pin_3#define LCD_D4_PORT GPIOA#define LCD_D5_PIN GPIO_Pin_4#define LCD_D5_PORT GPIOA#define LCD_D6_PIN GPIO_Pin_5#define LCD_D6_PORT GPIOA#define LCD_D7_PIN GPIO_Pin_6#define LCD_D7_PORT GPIOA​// 定义命令和数据的宏#define LCD_COMMAND 0#define LCD_DATA 1​// 延时函数,用于产生适当的延时void Delay(uint32_t nCount) { for (; nCount != 0; --nCount) { }}​// 发送命令或数据到LCD函数void LCD_Send(uint8_t byte, uint8_t mode) { GPIO_WriteBit(LCD_RS_PORT, LCD_RS_PIN, (mode == LCD_DATA) ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_RW_PORT, LCD_RW_PIN, Bit_RESET); GPIO_WriteBit(LCD_D4_PORT, LCD_D4_PIN, (byte >> 4) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D5_PORT, LCD_D5_PIN, (byte >> 5) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D6_PORT, LCD_D6_PIN, (byte >> 6) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D7_PORT, LCD_D7_PIN, (byte >> 7) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_SET); Delay(1000); GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_RESET); GPIO_WriteBit(LCD_D4_PORT, LCD_D4_PIN, (byte >> 0) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D5_PORT, LCD_D5_PIN, (byte >> 1) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D6_PORT, LCD_D6_PIN, (byte >> 2) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D7_PORT, LCD_D7_PIN, (byte >> 3) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_SET); Delay(1000); GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_RESET); Delay(1000);}​// 初始化LCD函数void LCD_Init(void) { Delay(45000); LCD_Send(0x30, LCD_COMMAND); Delay(4500); LCD_Send(0x30, LCD_COMMAND); Delay(150); LCD_Send(0x30, LCD_COMMAND); Delay(150); LCD_Send(0x20, LCD_COMMAND); Delay(150); LCD_Send(0x28, LCD_COMMAND); Delay(150);​ LCD_Send(0x08, LCD_COMMAND); Delay(150); LCD_Send(0x01, LCD_COMMAND); Delay(150); LCD_Send(0x06, LCD_COMMAND); Delay(150); LCD_Send(0x0C, LCD_COMMAND); Delay(150);}​// 在指定位置显示数字函数void LCD_DisplayNumber(uint8_t number, uint8_t x, uint8_t y) { uint8_t data = 0x30 + number; // 转换数字为对应的ASCII码 if (x >= 0 && x < 16 && y >= 0 && y < 2) { uint8_t addr = 0x80 + (y * 0x40) + x; LCD_Send(addr, LCD_COMMAND); LCD_Send(data, LCD_DATA); }}​int main(void) { // 初始化GPIO和LCD RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = LCD_RS_PIN | LCD_RW_PIN | LCD_E_PIN | LCD_D4_PIN | LCD_D5_PIN | LCD_D6_PIN | LCD_D7_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); LCD_Init(); while (1) { // 在第一行第一列显示数字1 LCD_DisplayNumber(1, 0, 0); }}​3.2 计时代码通过定时器2实现了收费计时功能,并在串口上打印出计时的实时时间。通过按下'S'键启动计时器,按下'Q'键停止计时器。每隔500毫秒,在串口上打印出实时时间。#include "stm32f10x.h"#include <stdio.h>​// 定义计时状态typedef enum { TIMER_STOPPED, TIMER_RUNNING} TimerState;​TimerState timerState = TIMER_STOPPED; // 计时器初始状态为停止uint32_t startTime = 0; // 开始计时的时间​// 初始化定时器2void Timer2_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);​ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1; // 设置预分频值,产生1ms的时间基准 TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 设置计数器的重载值,每1秒中断一次 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);​ TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE);​ NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);}​// 开始计时void StartTimer(void) { if (timerState == TIMER_STOPPED) { startTime = TIM_GetCounter(TIM2); // 记录开始计时的时间 timerState = TIMER_RUNNING; }}​// 停止计时void StopTimer(void) { if (timerState == TIMER_RUNNING) { timerState = TIMER_STOPPED; }}​// 获取实时时间,返回单位为毫秒uint32_t GetElapsedTime(void) { if (timerState == TIMER_RUNNING) { return TIM_GetCounter(TIM2) - startTime; } else { return 0; }}​// 初始化串口1void USART1_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);​ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX引脚 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);​ USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure);​ USART_Cmd(USART1, ENABLE);}​// 重定向printf函数到串口输出int fputc(int ch, FILE *f) { if (ch == '\n') { USART_SendData(USART1, '\r'); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); }​ USART_SendData(USART1, ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);​ return ch;}​int main(void) { // 初始化定时器和串口 Timer2_Init(); USART1_Init();​ printf("Press 'S' to start the timer.\r\n"); printf("Press 'Q' to stop the timer.\r\n");​ while (1) { if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) { uint8_t rxData = (uint8_t)USART_ReceiveData(USART1); if (rxData == 'S' || rxData == 's') { StartTimer(); printf("Timer started.\r\n"); } else if (rxData == 'Q' || rxData == 'q') { StopTimer(); printf("Timer stopped.\r\n"); } }​ // 每隔500毫秒打印实时时间 if (GetElapsedTime() >= 500) { printf("Elapsed time: %lu ms\r\n", GetElapsedTime()); startTime = TIM_GetCounter(TIM2); // 更新开始计时的时间 } }}​// 定时器2中断处理函数void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); }}​
  • [技术干货] 通过C语言设计的推箱子(控制台终端)
    一、项目介绍推箱子游戏是一款经典的益智小游戏,玩家需要控制主角角色将几个木箱按照要求推到指定位置。在控制台终端中,可以使用字符来表示不同的游戏元素,例如 '#' 表示墙壁, ' ' 表示空地, '$' 表示木箱, '@' 表示主角角色, '+' 表示完成任务的目标位置。实现步骤如下:定义常量和全局变量:需要定义一些常量和全局变量,用于存储游戏中的数据信息,游戏界面的宽度和高度、不同状态下的符号表示、木箱和目标位置等参数。同时还需要定义一个二维字符数组board,用于表示整个游戏界面。初始化游戏界面:在InitGame()函数中进行游戏初始化,设置游戏界面的边框和各个元素的位置。其中,可以使用循环遍历二维字符数组来设置元素的位置,将 '#' 设置为墙壁, ' ' 设置为空地,'$' 设置为木箱等。绘制游戏画面:DrawGame()函数用于绘制游戏画面,并将board数组中的字符逐行输出。可以使用循环遍历二维字符数组来进行输出。更新游戏状态:UpdateGame()函数用于更新游戏状态,包括判断主角角色是否可以移动,以及是否完成任务等操作。可以使用if语句和switch语句来判断不同情况下的操作,判断主角角色是否碰到墙壁或木箱,是否完成任务等。控制主角角色移动:Control()函数用于控制主角角色的移动,读取键盘输入并更新主角角色的位置。可以使用getch()函数获取键盘输入,并根据用户输入进行判断,按下方向键上时主角角色向上移动。判断游戏是否结束:CheckGameOver()函数用于检查游戏是否结束,包括是否成功完成任务或者失败结束游戏。如果判断出游戏结束,则直接退出程序。游戏暂停:Pause()函数用于控制游戏的暂停时间,可以通过调用Sleep()函数来实现。游戏结束:GameOver()函数用于输出最终的游戏得分和游戏结束信息,并直接退出程序。二、代码实现 #include <stdio.h> #include <conio.h> #include <windows.h> ​ //定义常量和全局变量 const int WIDTH = 11; const int HEIGHT = 10; const char WALL = '#'; const char EMPTY = ' '; const char BOX = '$'; const char TARGET = '+'; const char PLAYER = '@'; ​ int playerX, playerY, score; char board[HEIGHT][WIDTH]; ​ //初始化游戏界面 void InitGame() { //设置游戏界面的边框和各个元素的位置 for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { if (i == 0 || i == HEIGHT - 1 || j == 0 || j == WIDTH - 1) { board[i][j] = WALL; } else { board[i][j] = EMPTY; } } } ​ //设置木箱和目标位置 board[2][2] = BOX; board[4][5] = BOX; board[6][8] = BOX; board[2][8] = TARGET; board[4][2] = TARGET; board[6][5] = TARGET; ​ //设置主角角色位置 playerX = 7; playerY = 5; board[playerX][playerY] = PLAYER; } ​ //绘制游戏画面 void DrawGame() { system("cls"); for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { printf("%c", board[i][j]); } printf("\n"); } printf("Score: %d\n", score); } ​ //更新游戏状态 void UpdateGame(int dx, int dy) { int nextX = playerX + dx; int nextY = playerY + dy; ​ //判断主角角色是否可以移动 if (board[nextX][nextY] == EMPTY || board[nextX][nextY] == TARGET) { board[playerX][playerY] = EMPTY; playerX = nextX; playerY = nextY; board[playerX][playerY] = PLAYER; } else if (board[nextX][nextY] == BOX && (board[nextX + dx][nextY + dy] == EMPTY || board[nextX + dx][nextY + dy] == TARGET)) { //判断主角角色是否可以推动木箱 board[playerX][playerY] = EMPTY; playerX = nextX; playerY = nextY; board[playerX][playerY] = PLAYER; board[nextX + dx][nextY + dy] = BOX; board[nextX][nextY] = EMPTY; } ​ //判断是否完成任务 if (board[2][8] == BOX && board[4][2] == BOX && board[6][5] == BOX) { score += 100; printf("Congratulations! You win!\n"); Sleep(2000); exit(0); } } ​ //控制主角角色移动 void Control() { char c = getch(); switch (c) { case 'w': UpdateGame(-1, 0); break; case 's': UpdateGame(1, 0); break; case 'a': UpdateGame(0, -1); break; case 'd': UpdateGame(0, 1); break; default: break; } } ​ //判断游戏是否结束 void CheckGameOver() { if (board[playerX - 1][playerY] == WALL || board[playerX + 1][playerY] == WALL || board[playerX][playerY - 1] == WALL || board[playerX][playerY + 1] == WALL) { printf("Game over! You lose!\n"); Sleep(2000); exit(0); } } ​ //游戏暂停 void Pause() { Sleep(100); } ​ //游戏结束 void GameOver() { printf("Your final score is: %d\n", score); exit(0); } ​ int main() { //初始化游戏界面 InitGame(); ​ //游戏循环 while (1) { DrawGame(); Control(); CheckGameOver(); Pause(); } ​ return 0; } ​
  • [问题求助] STM32F407VGTx的SPI作为从机调用 hal库API 数据传输问题
    MCU:STM32F407VGTx工具:STM32CubeMX问题:MCU的spi1(从) 与 SOC spi(主) 数据进行传输;调用库函数:HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout)接收和发送没有问题,接收发送数据都OK但是调用HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size)HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size)MISO和MOSI的数据都会出现传输错误;代码:循环调用HAL_SPI_TransmitReceive
  • [技术干货] 通过C语言设计的贪吃蛇游戏(控制台终端)
    一、项目介绍当前通过控制台终端实现一个贪吃蛇小游戏,实现游戏的绘制、更新、控制等功能。二、实现效果三、完整代码下面贴出的代码在Windows系统上编译运行,需要使用conio.h头文件中的getch()函数来获取键盘输入,用于控制蛇的移动。可以通过按下'w'、's'、'a'、'd'四个键来分别控制蛇的向上、向下、向左、向右移动。同时还有一个暂停函数Pause(),用于控制蛇的移动速度,可以根据需要调整暂停时间。设计思路:程序定义了一些常量,包括游戏界面的宽度和高度、不同状态下的符号表示等,并在全局变量中定义了蛇头、蛇身、食物等的位置、得分、长度、行进方向等参数。同时还定义了一个二维字符数组board,用于表示整个游戏界面。在InitGame()函数中进行游戏初始化,设置游戏界面的边框、蛇头、蛇身、随机生成食物等操作。DrawGame()函数用于绘制游戏画面,将board数组中的字符逐行输出,并在末尾输出得分。UpdateGame()函数用于更新游戏状态,包括蛇的移动、游戏结束判断等。CheckGameOver()函数用于检查游戏是否结束,包括墙壁碰撞、蛇身碰撞、得分达到上限等情况。GenerateFood()函数用于随机生成食物位置,调用rand()函数获取随机数并进行判断。Move()函数用于蛇的移动,根据当前行进方向进行移动。Control()函数用于用户操作,读取键盘输入并更新蛇的行进方向。Pause()函数用于控制游戏速度,通过调用Sleep()函数控制暂停时间。GameOver()函数用于输出最终得分和游戏结束信息,并直接退出程序。 #include <stdio.h> #include <stdlib.h> #include <conio.h> //需要使用getch()函数 #include <time.h> //需要使用time()函数 #include <Windows.h> ​ ​ #define WIDTH 40 //游戏界面宽度 #define HEIGHT 20 //游戏界面高度 ​ //定义符号常量,表示各种不同的状态 #define BLANK ' ' //空白 #define WALL '*' //墙壁 #define SNAKEHEAD '@' //蛇头 #define SNAKEBODY 'o' //蛇身 #define FOOD '$' //食物 ​ //定义坐标结构体 struct Position { int x; //横坐标 int y; //纵坐标 }; ​ //定义枚举类型,表示游戏状态 enum GameState { Over = -1, //游戏结束 Running = 0, //游戏进行中 Win = 1 //游戏胜利 }; ​ int score = 0; //得分 struct Position head; //蛇头位置 struct Position body[WIDTH * HEIGHT]; //蛇身位置 struct Position food; //食物位置 char board[WIDTH][HEIGHT]; //游戏界面 int length = 3; //蛇身长度,初始为3 int direction = 0; //蛇的行进方向,0表示向右,1表示向下,2表示向左,3表示向上 ​ //函数声明 void InitGame(); //初始化游戏界面和蛇的初始位置 void DrawGame(); //绘制游戏画面 void UpdateGame(); //更新游戏状态 enum GameState CheckGameOver(); //检查游戏是否结束 void GenerateFood(); //生成随机食物 void Move(); //蛇的移动 void Control(); //用户操作,控制蛇的移动 void Pause(); //游戏暂停 void GameOver(); //游戏结束 ​ int main() { srand(time(NULL)); //用当前时间作为随机数种子,使每次运行的随机食物位置不同 InitGame(); //初始化游戏 DrawGame(); //绘制游戏画面 while (1) { UpdateGame(); //更新游戏 DrawGame(); //绘制游戏画面 Control(); //用户操作,控制蛇的移动 Pause(); //游戏暂停 } return 0; } ​ //初始化游戏界面和蛇的初始位置 void InitGame() { for (int i = 0; i < WIDTH; i++) { for (int j = 0; j < HEIGHT; j++) { if (i == 0 || j == 0 || i == WIDTH - 1 || j == HEIGHT - 1) //设置墙壁 board[i][j] = WALL; else board[i][j] = BLANK; //其他为空白 } } ​ //初始化蛇的位置,由一个蛇头和两节身体组成,初始位置在游戏界面的中心 head.x = WIDTH / 2; head.y = HEIGHT / 2; board[head.x][head.y] = SNAKEHEAD; ​ body[0].x = head.x - 1; body[0].y = head.y; board[body[0].x][body[0].y] = SNAKEBODY; ​ body[1].x = head.x - 2; body[1].y = head.y; board[body[1].x][body[1].y] = SNAKEBODY; ​ GenerateFood(); //生成随机食物 } ​ //绘制游戏画面 void DrawGame() { system("cls"); //清屏,避免前一帧的内容残留 ​ for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { printf("%c", board[j][i]); //输出一个字符 } printf("\n"); //每行输出完后,换行 } ​ printf("Score: %d\n", score); //输出得分 } ​ //更新游戏状态 void UpdateGame() { Move(); //蛇的移动 enum GameState state = CheckGameOver(); //检查游戏是否结束 if (state != Running) //如果游戏结束,则进行相应操作 { GameOver(); } } ​ //检查游戏是否结束 enum GameState CheckGameOver() { //蛇头碰到墙壁,游戏结束 if (board[head.x][head.y] == WALL) return Over; ​ //蛇头碰到蛇身,游戏结束 for (int i = 0; i < length; i++) { if (head.x == body[i].x && head.y == body[i].y) return Over; } ​ //吃到食物后,更新分数和蛇的长度,并生成新的食物 if (head.x == food.x && head.y == food.y) { score += 10; length++; GenerateFood(); } ​ //蛇的长度达到游戏界面总格子数减去墙壁的个数,也就是蛇填满游戏界面,游戏胜利 if (length == (WIDTH - 2) * (HEIGHT - 2) - 4) return Win; ​ return Running; //游戏继续进行 } ​ //生成随机食物 void GenerateFood() { int x, y; do { x = rand() % (WIDTH - 2) + 1; //随机x坐标,排除在边框上的墙壁位置 y = rand() % (HEIGHT - 2) + 1; //随机y坐标,排除在边框上的墙壁位置 } while (board[x][y] != BLANK); //如果随机到的位置不为空白,则重新随机 food.x = x; food.y = y; board[x][y] = FOOD; //在随机位置生成食物 } ​ //蛇的移动 void Move() { //更新蛇身的位置,从后往前移动 for (int i = length - 1; i > 0; i--) { body[i].x = body[i - 1].x; body[i].y = body[i - 1].y; board[body[i].x][body[i].y] = SNAKEBODY; } ​ //更新蛇头的位置 switch (direction) //根据蛇头行进方向进行移动 { case 0: //向右 head.x++; break; case 1: //向下 head.y++; break; case 2: //向左 head.x--; break; case 3: //向上 head.y--; break; } board[head.x][head.y] = SNAKEHEAD; //更新蛇头位置 board[body[length - 1].x][body[length - 1].y] = BLANK; //清除蛇尾 } ​ //用户操作,控制蛇的移动 void Control() { if (kbhit()) //如果有按键按下 { char ch = getch(); //获取按键字符 switch (ch) { case 'w': if (direction != 1) //避免蛇头掉头 direction = 3; break; case 's': if (direction != 3) direction = 1; break; case 'a': if (direction != 0) direction = 2; break; case 'd': if (direction != 2) direction = 0; break; case 'q': GameOver(); //按下'q'键退出游戏 break; } } } ​ //游戏暂停 void Pause() { Sleep(150); //每次循环暂停一段时间,控制蛇的移动速度 } ​ //游戏结束 void GameOver() { system("cls"); //清屏,输出最终得分和游戏结束信息 printf("Game over!\n"); printf("Your score: %d\n", score); exit(0); //直接退出程序 }
  • [技术干货] 通过51单片机实现直流电机调速
    一、项目背景及目的随着各种工业生产设备和机械设备的广泛使用,直流电机调速技术的研究和应用越来越受到人们的重视,具有广泛的应用前景。本项目通过51单片机实现直流电机调速功能,为实际工程应用提供一个可靠和有效的调速方案。二、设计思路(1)系统原理本系统采用PWM(脉冲宽度调制)技术对直流电机进行调速控制。通过改变输出信号的占空比,实现对直流电机的转速控制。系统中包括51单片机、直流电机、电路板以及控制程序。(2)硬件设计电机:使用24V直流电机实现实际转速控制。驱动电路:使用四个寄生二极管三相全桥驱动电路控制电机,使电机可以正反转,并控制电机的速度。51单片机:使用STC89C52单片机,作为控制核心。单片机通过捕捉外部信号和计算控制电压来实现对电机的转速控制。同时还需通过编写程序来控制电机的启动、停止等操作。显示器:使用1602LCD显示屏,显示转速和其他操作信息。电源:使用24V直流电源作为系统的电源。(3)软件设计采用C语言编写单片机程序进行控制。实现PWM技术控制直流电机的转速。通过调整占空比来改变输出电压,从而达到控制直流电机转速的目的。使用定时器模块实现计数来测量电机的转速,并通过显示器实时显示。设定按键和旋钮控制,如启动、停止电机等。三、设计代码 #include <reg52.h> ​ sbit MotorP = P1^0; //定义电机正极口 sbit MotorN = P1^1; //定义电机负极口 float V_motor = 0; //定义电机控制电压 unsigned int speed = 0; //定义电机转速 ​ //初始化函数 void Init() { //定时器计数器及工作模式设置 TMOD |= 0x01; //T0定时器模式1 TH0 = 0xfc; //定时计数最大值,控制PWM频率 TL0 = 0x00; //初值为0 TR0 = 1; //启动T0定时器 ​ //ADC设置 ADC_CONTR = 0x84; //启动AD转换器 } ​ //ADC采样函数 float ADConvert() { ADC_CONTR &= 0xEF; //清除AD转换结束标志位 ADC_CONTR |= 0x40; //启动AD转换 while(!(ADC_CONTR & 0x10)); //等待转换完成 return ADC_RES; //返回转换结果 } ​ //计算电机控制电压函数 void ControlMotor() { unsigned int value = ADConvert(); //采集电位器输出 V_motor = (value / 1023.0) * 5; //根据电压分压公式计算电机控制电压 } ​ //控制电机函数 void DriveMotor() { if(V_motor >= 2.5) //当电位器输出电压大于2.5V时电机正转,当小于2.5V时电机反转 { MotorP = 1; MotorN = 0; } else if(V_motor < 2.5) { MotorP = 0; MotorN = 1; } ​ speed = 60 * 1000 / (3 * TH0 * 12); //根据定时器计数值计算电机转速 } ​ //显示函数 void Display() { //将电机转速和状态信息显示在LCD显示屏上 } ​ //主函数 void main() { Init(); //初始化函数 ​ while(1) { ControlMotor(); //计算电机控制电压 DriveMotor(); //控制电机运行 Display(); //显示电机状态 } } ​
  • [技术干货] 基于STM32设计的自动刹车灯
    一、项目介绍随着科技的发展,人们对低碳环保的认知和需求不断提高。骑自行车既能够低碳环保,又能够锻炼身体,成为了很多人出行的首选。然而,由于自行车本身没有带指示灯,比如刹车指示灯等,所以自行车的安全性并不是很好,如果人们在骑自行车时紧急刹车,后车无法及时判断前方自行车的行为,容易造成交通事故。因此,设计一款自动刹车灯系统具有十分重要的意义。本项目实现了通过安装ADXL345陀螺仪和四枚LED灯还有STM32F103C8T6主控芯片来实现自行车自动刹车灯的功能。当自行车上安装了该设备后,ADXL345通过IIC通信协议将X,Y,Z三轴的加速度实时值发送给SMT32F103C8T6主控芯片,并结合STM32高级定时器的PWM功能,输出不同占空比的脉冲,控制不同的LED灯输出多种亮度等级,从而控制不同的LED的开关以及明暗,并且通过不同亮度的红光和绿光混合,能够得到黄色的LED灯光。这样,在自行车急刹或者加速时,实时地控制LED灯的亮度和颜色,让后方车辆能够更清楚地了解前方自行车的行为,从而做出快速的反应,保障骑行者以及后车的安全。同时,该系统也能够提高自行车的可见性,并且对于追求低碳环保的人群来说,让自行车既能低碳环保,又能够锻炼身体。二、设计思路2.1 项目目标本项目通过安装ADXL345陀螺仪和四枚LED灯还有STM32F103C8T6主控芯片来实现自行车自动刹车灯的功能,使得自行车在急刹或者加速时,实时地控制LED灯的亮度和颜色,提高其可见性,降低交通事故的风险。同时,该系统还能够使自行车既能低碳环保,又能够锻炼身体。2.2 项目硬件构成(1)自行车:作为安装系统的物体,需要有一个固定的位置来安装ADXL345陀螺仪和四枚LED灯。(2)ADXL345陀螺仪:通过IIC通信协议与STM32F103C8T6主控芯片通信,并将X、Y、Z三轴的加速度实时值发送给SMT32F103C8T6主控芯片。(3)四枚LED灯:使用不同亮度的红光和绿光混合,能够得到黄色的LED灯光。通过控制其亮度和颜色来提高自行车的可见性。(4)STM32F103C8T6主控芯片:根据接收到的ADXL345数据,结合STN32的高级定时器的PWM功能,输出不同占空比的脉冲,控制不同的LED灯输出多种亮度等级。2.3 项目功能实现(1)自行车加速度监测:ADXL345陀螺仪通过IIC通信协议与STM32F103C8T6主控芯片通信,实时地感知自行车的加速度变化。(2)LED灯亮度和颜色控制:STM32F103C8T6主控芯片运用高级定时器的PWM功能,能够输出不同占空比的脉冲,并控制不同的LED灯输出多种亮度等级,通过不同亮度的红光和绿光混合,能够得到黄色的LED灯光,提高自行车的可见性。(3)系统安装和调试:需要将ADXL345陀螺仪和四枚LED灯与STM32F103C8T6主控芯片连接起来,并进行系统测试和调试。三、系统测试3.1 功能样机安装与焊接绘制好电路原理图之后,按照原理图将自动刹车灯系统的各个模块安装在事先购买好的洞洞板上,然后用导线将他们连接在一起,最后再焊接在一起,做成完整的自动刹车灯电路板。3.2 ADXL345模块调试当上电后,将自动刹车灯电路的串口2外设引脚连接至PC端,将加速度解算后的实际值发送至PC端,通过PC端串口调试助手显示出具体数值,再观察数值是否符合常理。通过显示的数据信息,可以推测出ADXL345陀螺仪能够正常工作。3.3 实物调试最后阶段,将对自行车自动刹车灯进行实物调试,确定其基本功能能够正常实现。当系统上电后,左右各一枚LED发出低亮黄色灯光,如下图。静置30S后,所有LED均熄灭,如下图。当检测到震动后,重新亮起两盏黄色LED灯,如下图。​当检测到刹车时,四枚LED灯均以高亮发出红色灯光,如下图。结合自行车自动刹车灯的功能需求和实物调试结果,可以发现,调试结果完全符合自动刹车灯的预期功能。四、代码设计4.1 主函数 #include "stm32f10x.h" #include "usart.h" #include "led.h" #include "RTC_Time.h" #include <stdio.h> #include "delay.h" #include "sys.h" #include "stdlib.h" #include "stdio.h" #include "string.h" #include "adxl345.h" int main(void) { u32 flag=0; short x, y, z; float accelerated; LED_GPIO_Config();//初始化LED USART2_Config(); delay_init(); //延时函数初始化 PWM_LED_INIT(); //PWM PA8-9 LED_Init(); //PB7 LED-R PBout(7) = 1; ADXL345_Init(); //PB 10,11 ADXL345_Read_Average(&x, &y, &z, 20); ADXL345_AUTO_Adjust((char *)&x, (char *)&y, (char *)&z); TIM_SetCompare1(TIM1, 50); //设置TIMx捕获比较1寄存器(通道1)值(脉冲宽度) 占空比%20 TIM_SetCompare2(TIM1, 50); //设置TIMx捕获比较2寄存器(通道2)值(脉冲宽度) 占空比%20 while (1) { ADXL345_Read_Average(&x, &y, &z, 5); //读加速度值 accelerated=(x*3.9/1000*9.8); //加速度实际值 printf("X=%4.1f Y=%4.1f Z=%4.1f\r\n",accelerated,(y*3.9/1000*9.8),(z*3.9/1000*9.8)); while(flag>425) { TIM_SetCompare1(TIM1, 0); //通道2 占空比%0 TIM_SetCompare2(TIM1, 0); //通道2 占空比%0 ADXL345_Read_Average(&x, &y, &z, 5); accelerated=(x*3.9/1000*9.8); if(accelerated<-5||accelerated>5) { break; } } flag++; if(accelerated<-4) { //四个LED低电平导通 TIM_SetCompare1(TIM1, 0); //GREEN不亮 TIM_SetCompare2(TIM1, 1000); //RED高亮 PBout(7) = 0; flag=0; } if(accelerated>0) { PBout(7) = 1; TIM_SetCompare1(TIM1, 50); //RED低亮 TIM_SetCompare2(TIM1, 50); //GREEN低亮 } if(accelerated>5) { flag=0; } } }4.2 LED灯控制 #include "led.h" #include "delay.h" void LED_GPIO_Config(void) { //定义一个GPIO_InitTypeDef 类型的结构体,名字叫GPIO_InitStructure GPIO_InitTypeDef GPIO_InitStructure; //使能GPIOC的外设时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); //选择要用的GPIO引脚 GPIO_InitStructure.GPIO_Pin =GPIO_Pin_13; ///设置引脚模式为推免输出模式 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置引脚速度为50MHZ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //调用库函数,初始化GPIO GPIO_Init(GPIOC, &GPIO_InitStructure); } void TIME_INIT() { TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; //根据TIM_OCInitStruct中指定的参数初始化外设TIMx RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); //TIM1定时器初始化 10ms TIM_TimeBaseInitStructure.TIM_Period = 999; TIM_TimeBaseInitStructure.TIM_Prescaler = 719; TIM_TimeBaseInitStructure.TIM_ClockDivision = 0; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStructure); //TIM1的PWM配置 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_Pulse = 0;//设置初始PWM脉冲宽度为0 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //PWM输出使能 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;//当定时器计数值小于CCR_Val时为低电平 //通道的使能 TIM_OC1Init(TIM1, &TIM_OCInitStructure); //通道1 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2Init(TIM1, &TIM_OCInitStructure); //通道2 TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); //使能TIM1重载寄存器ARR TIM_Cmd(TIM1, ENABLE); //使能 TIM_CtrlPWMOutputs(TIM1, ENABLE); //高级定时器必须加 } void PWM_LED_INIT(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); //GPIOA8,9,10是TIM1的通道1,2,3 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 ; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); TIME_INIT(); } void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; //RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); } ​4.3 adxl345.c #include "adxl345.h" #include "sys.h" #include "delay.h" #include "math.h" u8 ADXL345_Init(void) { IIC_Init(); //初始化IIC总线 if(ADXL345_RD_Reg(DEVICE_ID)==0XE5) //读取器件ID { ADXL345_WR_Reg(0X31,0X2B); //低电平中断输出,13位全分辨率,输出数据右对齐,16g量程 ADXL345_WR_Reg(0X2C,0x0A); //数据输出速度为100Hz ADXL345_WR_Reg(0X2D,0x28); //链接使能,测量模式 ADXL345_WR_Reg(0X2E,0x00); //不使用中断 ADXL345_WR_Reg(0X1E,0x00); ADXL345_WR_Reg(0X1F,0x00); ADXL345_WR_Reg(0X20,0x00); return 0; } return 1; } //写ADXL345寄存器 //addr:寄存器地址 //val:要写入的值 //返回值:无 void ADXL345_WR_Reg(u8 addr,u8 val) { IIC_Start(); IIC_Send_Byte(ADXL_WRITE); //发送写器件指令 IIC_Wait_Ack(); IIC_Send_Byte(addr); //发送寄存器地址 IIC_Wait_Ack(); IIC_Send_Byte(val); //发送值 IIC_Wait_Ack(); IIC_Stop(); //产生一个停止条件 } //读ADXL345寄存器 //addr:寄存器地址 //返回值:读到的值 u8 ADXL345_RD_Reg(u8 addr) { u8 temp=0; IIC_Start(); IIC_Send_Byte(ADXL_WRITE); //发送写器件指令 temp=IIC_Wait_Ack(); IIC_Send_Byte(addr); //发送寄存器地址 temp=IIC_Wait_Ack(); IIC_Start(); //重新启动 IIC_Send_Byte(ADXL_READ); //发送读器件指令 temp=IIC_Wait_Ack(); temp=IIC_Read_Byte(0); //读取一个字节,不继续再读,发送NAK IIC_Stop(); //产生一个停止条件 return temp; //返回读到的值 } //读取ADXL的平均值 //x,y,z:读取10次后取平均值 void ADXL345_RD_Avval(short *x,short *y,short *z) { short tx=0,ty=0,tz=0; u8 i; for(i=0;i<10;i++) { ADXL345_RD_XYZ(x,y,z); delay_ms(10); tx+=(short)*x; ty+=(short)*y; tz+=(short)*z; } *x=tx/10; *y=ty/10; *z=tz/10; } //自动校准 //xval,yval,zval:x,y,z轴的校准值 void ADXL345_AUTO_Adjust(char *xval,char *yval,char *zval) { short tx,ty,tz; u8 i; short offx=0,offy=0,offz=0; ADXL345_WR_Reg(POWER_CTL,0x00); //先进入休眠模式. delay_ms(100); ADXL345_WR_Reg(DATA_FORMAT,0X2B); //低电平中断输出,13位全分辨率,输出数据右对齐,16g量程 ADXL345_WR_Reg(BW_RATE,0x0A); //数据输出速度为100Hz ADXL345_WR_Reg(POWER_CTL,0x28); //链接使能,测量模式 ADXL345_WR_Reg(INT_ENABLE,0x00); //不使用中断 ADXL345_WR_Reg(OFSX,0x00); ADXL345_WR_Reg(OFSY,0x00); ADXL345_WR_Reg(OFSZ,0x00); delay_ms(12); for(i=0;i<10;i++) { ADXL345_RD_Avval(&tx,&ty,&tz); offx+=tx; offy+=ty; offz+=tz; } offx/=10; offy/=10; offz/=10; *xval=-offx/4; *yval=-offy/4; *zval=-(offz-256)/4; ADXL345_WR_Reg(OFSX,*xval); ADXL345_WR_Reg(OFSY,*yval); ADXL345_WR_Reg(OFSZ,*zval); } //读取3个轴的数据 //x,y,z:读取到的数据 void ADXL345_RD_XYZ(short *x,short *y,short *z) { u8 buf[6]; u8 i; IIC_Start(); IIC_Send_Byte(0X3A); //发送写器件指令 IIC_Wait_Ack(); IIC_Send_Byte(0x32); //发送寄存器地址(数据缓存的起始地址为0X32) IIC_Wait_Ack(); IIC_Start(); //重新启动 IIC_Send_Byte(0X3B); //发送读器件指令 IIC_Wait_Ack(); for(i=0;i<6;i++) { if(i==5)buf[i]=IIC_Read_Byte(0);//读取一个字节,不继续再读,发送NACK else buf[i]=IIC_Read_Byte(1); //读取一个字节,继续读,发送ACK delay_us(15); IIC_Start(); //重新启动 IIC_Send_Byte(0X3A); //发送写器件指令 IIC_Wait_Ack(); IIC_Send_Byte(0x33+i); //发送寄存器地址(数据缓存的起始地址为0X32) IIC_Wait_Ack(); IIC_Start(); //重新启动 IIC_Send_Byte(0X3B); //发送读器件指令 IIC_Wait_Ack(); } IIC_Stop(); //产生一个停止条件 *x=(short)(((u16)buf[1]<<8)+buf[0]); *y=(short)(((u16)buf[3]<<8)+buf[2]); *z=(short)(((u16)buf[5]<<8)+buf[4]); } //读取ADXL345的数据times次,再取平均 //x,y,z:读到的数据 //times:读取多少次 void ADXL345_Read_Average(short *x,short *y,short *z,u8 times) { u8 i; short tx,ty,tz; *x=0; *y=0; *z=0; if(times)//读取次数不为0 { for(i=0;i<times;i++)//连续读取times次 { ADXL345_RD_XYZ(&tx,&ty,&tz); *x+=tx; *y+=ty; *z+=tz; delay_ms(5); } *x/=times; *y/=times; *z/=times; } } //得到角度 //x,y,z:x,y,z方向的重力加速度分量(不需要单位,直接数值即可) //dir:要获得的角度.0,与Z轴的角度;1,与X轴的角度;2,与Y轴的角度. //返回值:角度值.单位0.1°. short ADXL345_Get_Angle(float x,float y,float z,u8 dir) { float temp; float res=0; switch(dir) { case 0://与自然Z轴的角度 temp=sqrt((x*x+y*y))/z; res=atan(temp); break; case 1://与自然X轴的角度 temp=x/sqrt((y*y+z*z)); res=atan(temp); break; case 2://与自然Y轴的角度 temp=y/sqrt((x*x+z*z)); res=atan(temp); break; } return res*1800/3.14; }
  • [专题汇总] 7月嵌入式项目开发专题总汇
    一、前言当前文章总结了14个基于不同单片机(51单片机和STM32)设计的软件和硬件项目。这些项目涵盖了计算器、手机、酒精检测仪、环境温度与湿度检测设备、考勤系统、门禁照相机、人体健康检测仪、数码相册、太阳能热水器、数显热水器和智能空调等多个领域。这些项目展示了单片机在不同领域的应用。通过这些项目,可以学习到如何设计和开发基于单片机的硬件和软件系统。其中,基于51单片机和STM32的项目涉及了计算器、手机、酒精检测仪、环境温度与湿度检测设备、考勤系统、门禁照相机、人体健康检测仪、数码相册、太阳能热水器、数显热水器和智能空调等多个领域。这些项目不仅展示了单片机的强大功能,还提供了实际应用的示例和参考。无论是初学者还是有经验的开发者,都可以从这些项目中获得有价值的知识和经验,进一步拓展自己在单片机开发领域的能力。二、项目实例【1】基于51单片机设计的计算器cid:link_3 项目里采用了单片机的IO口、定时器和LCD1602显示屏等技术原理。其中,IO口用于控制矩阵键盘、蜂鸣器和LCD1602显示屏等外设;定时器用于进行键盘扫描,确保能够准确地捕捉到按键的输入;LCD1602显示屏用于显示输入的数字和计算结果。设计思路主要分为三个部分:键盘扫描、计算器运算和LCD1602显示。在键盘扫描部分,通过定时器中断的方式进行键盘扫描,判断是否有按键按下,并将按键对应的数字保存到缓存区中。在计算器运算部分,采用栈的数据结构进行计算器运算,当按下运算符号时,将之前输入的数字压入栈中,等待下一次输入。当按下“=”时,从栈中取出数字进行计算,并将结果保存到栈中。最后将结果从栈中取出,显示在LCD1602显示屏上。在LCD1602显示部分,通过设置LCD1602的命令和数据,可以实现在LCD1602上显示数字和运算符号等内容。【2】基于STM32设计的简易手机cid:link_4 基于STM32设计的简易手机可以作为智能手表的模型进行开发,方便老人和儿童佩戴。项目主要是为了解决老年人或儿童使用智能手表时可能遇到的困难,例如操作困难、功能复杂等问题。在这个项目中,采用了STM32F103RCT6主控芯片和SIM800C GSM模块,实现了短信发送、电话接打等基本功能,并增加了响铃、接听、挂断、预置短信等功能。当检测到新的电话来时,会通过蜂鸣器通知用户,并通过按键进行接电话和挂电话,使操作更加简单易懂。手机还提供4个按键,可以向预先指定的联系人发送4条预置短信,更方便快捷。【3】基于STM32设计的酒精检测仪cid:link_5 随着社会的发展和生活水平的提高,人们对于行车安全、家庭安全的要求越来越高,而酒驾等问题也日渐突出,为此,开发一款基于STM32的酒精检测仪,通过检测酒精浓度,实时显示结果并进行报警,可以有效避免因酒后驾车带来的安全隐患。【4】基于51单片机+SHT30设计的环境温度与湿度检测设备(IIC模拟时序)cid:link_6 当前文章介绍基于51单片机和SHT30传感器设计的环境温度与湿度检测设备。设备采用IIC模拟时序通信协议,能够实时监测环境的温度和湿度,并将数据通过LCD显示屏显示出来;可以广泛应用于室内环境监测、气象观测、农业温室监测等领域。在本项目中,使用了51单片机作为主控芯片,SHT30传感器作为温湿度传感器,LCD显示屏作为数据显示模块。通过51单片机的GPIO口模拟IIC通信协议,实现了与SHT30传感器的数据通信。【5】基于STM32+SHT30设计的环境温度与湿度检测系统(IIC模拟时序)cid:link_7当前介绍基于STM32F103ZCT6芯片设计的环境温度与湿度检测系统设计过程。当前系统通过SHT30温湿度传感器采集环境温度和湿度数据,并通过模拟IIC时序协议将数据传输到STM32芯片上。然后,STM32芯片通过处理这些数据并将它们显示在0.91寸OLED显示屏上,以便用户能够方便地观察环境温度和湿度的变化情况。系统的主控芯片采用了STM32F103ZCT6,这是一款高性能的32位ARM Cortex-M3微控制器,具有丰富的外设和存储器资源,可满足各种应用的需求。温湿度检测传感器采用了SHT30,这是一款高精度的数字式温湿度传感器,具有快速响应、低功耗、高可靠性等特点。【6】Qt使用kingbase数据库存储数据(完成考勤系统数据增删改查)cid:link_8 当前基于Qt(C++)开发了一款教室上课考勤系统的软件,主要是使用了Kingbase数据库进行数据存储和管理。完成的具体功能如下:(1)功能齐全:软件可以完成学生、教师和管理员的登陆和注册,教师可以发布课程信息和考勤信息,学生可以查看自己的课程信息和考勤记录,管理员可以对教师和学生信息进行管理。软件具有数据可视化等功能,方便管理员直观地了解教学情况。(2)高效稳定:采用了Kingbase数据库存储数据,保证了数据存储的可靠性和一致性,同时也提高了系统性能和响应速度。在程序设计方面采用了MVC模式,将程序的逻辑与界面分离,使得程序结构清晰,易于维护和扩展。(3)用户友好:采用了人性化的操作界面和交互方式,让用户能够方便地浏览和管理课程和考勤记录。考虑到了软件的安全性问题,采用了哈希加密算法保护用户密码。【7】C语言里变量的生命周期cid:link_0 在 C 语言中,变量的生命周期指的是该变量存在的时间段,理解变量的内存释放时机,设计程序才能少出问题。在程序执行期间,变量会经历以下三个阶段:(1)定义阶段(定义变量):在定义变量时,编译器会为该变量分配内存空间。此时变量的值是不确定的。(2)使用阶段(赋值、读取变量):在程序执行过程中,可以对变量进行赋值或读取操作。此时变量的值是确定的,并且会随着程序执行的进度而变化。(3)销毁阶段(变量被销毁):在变量的作用域结束时,该变量就会被销毁。在这个过程中,编译器会自动释放该变量所占用的内存空间。【8】SQLite数据库完成数据增删改查cid:link_9当前文章介绍的设计的主要功能是利用 SQLite 数据库实现宠物投喂器上传数据的存储,并且支持数据的增删改查操作。其中,宠物投喂器上传的数据包括投喂间隔时间、水温、剩余重量等参数。实现功能:创建 SQLite 数据库表,用于存储宠物投喂器上传的数据。实现对数据库表中数据的插入操作,即将从宠物投喂器接收到的数据存储到数据库中。实现对数据库表中数据的查询操作,包括按照投喂间隔时间、水温、剩余重量等参数进行筛选,以便用户能够查看特定范围内的数据信息。实现对数据库表中数据的修改操作,即可以修改已经存储的宠物投喂器上传的数据。实现对数据库表中数据的删除操作,即可以删除已经存储的宠物投喂器上传的数据。【9】基于STM32设计的门禁照相机cid:link_1 当前文章介绍基于STM32设计的门禁照相机,本项目提供了一种更加智能、安全、便捷的门禁解决方案。门禁照相机采用STM32F103ZET6 MCU作为主控芯片,配合2.8寸LCD显示屏、OV7725数字摄像头、SD卡和模拟门铃按键等外设模块,实现了摄像头画面实时显示、门铃触发拍照、图片存储等功能。在使用该门禁照相机时,来访客人只需按下门铃按键,摄像头即可自动拍摄照片并保存到SD卡中。同时,用户也可以通过LCD屏幕进行时间调整和本地图片浏览等操作,提高了门禁系统的可操作性和用户体验。门禁照相机的设计为了提高门禁系统的安全性和智能化程度,解决传统门禁系统存在的诸多问题。通过采用数字摄像头替代传统猫眼,并实现照片自动拍摄和存储功能,有效提高了门禁系统的安全性。同时,通过LCD屏幕进行时间调整和本地图片浏览等操作,实现了门禁系统的智能化,提高了用户的使用体验。【10】基于STM32设计的人体健康检测仪cid:link_10 当前文章介绍基于STM32设计的人体健康检测仪。设备采用STM32系列MCU作为主控芯片,配备血氧浓度传感器(使用MAX30102血氧浓度检测传感器)、OLED屏幕和电池供电等外设模块。设备可以广泛应用于医疗、健康等领域。可以帮助医生和病人更好地了解病情变化,提高治疗效果和生活质量。设备也可以用于健康管理、运动监测等场景,帮助用户了解自己的身体状况,保持健康的生活方式。在项目中,使用了KEIL作为开发平台和工具,通过血氧模块采集人体的心跳和血氧浓度参数,并通过OLED屏幕显示现在的心跳和血氧浓度。同时,通过指标分析,提供采集到的数据与正常指标比对,分析被检测人员的健康状态。采集的数据可通过蓝牙或者WIFI传递给手机APP进行处理,方便用户随时了解自己的身体状况。本设计采用STM32为主控芯片,搭配血氧浓度传感器和OLED屏幕,实现了人体健康数据的采集和展示,并对采集到的数据进行分析,判断被检测人员的健康状态。同时,设计使用蓝牙或WiFi将采集到的数据传递给手机APP进行处理。【11】基于STM32设计的数码相册cid:link_2 项目是基于STM32设计的数码相册,能够通过LCD显示屏解码显示主流的图片,支持bmp、jpg、gif等格式。用户可以通过按键或者触摸屏来切换图片,同时还可以旋转显示,并能够自适应居中显示,小尺寸图片居中显示,大尺寸图片自动缩小显示(超出屏幕范围)。图片从SD卡中获取。【12】基于STM32设计的太阳能热水器cid:link_11 本项目使用 STM32F103C8T6 微控制器作为核心处理器,结合多个传感器和执行器,实现了太阳能热水器的自动控制。通过对光照、温度、水位等各种参数的监测和分析,对水泵、电磁阀等设备进行自动控制,从而实现太阳能热水器的高效、安全、可靠运行。【13】基于STM32设计的数显热水器cid:link_12当前介绍的项目是基于 STM32F103ZET6 系列 MCU 设计的数显热水器,通过显示屏来显示热水器的温度及其工作状态,通过 PT100 传感器来检测热水器的温度变化,并通过电加热片实现加热过程,以达到控制热水器温度的目的。【14】基于STM32设计的智能空调cid:link_13 随着人们生活水平的不断提高,对居住环境的舒适度要求也越来越高。空调作为一种重要的家电设备,已经成为了现代家庭中必不可少的一部分。本文介绍了一种基于STM32的智能空调设计方案,可以自动地根据环境温度进行温度调节。
  • [技术干货] 基于STM32设计的智能空调
    一、项目背景随着人们生活水平的不断提高,对居住环境的舒适度要求也越来越高。空调作为一种重要的家电设备,已经成为了现代家庭中必不可少的一部分。本文介绍了一种基于STM32的智能空调设计方案,可以自动地根据环境温度进行温度调节。最终的成品的模型如下:二、设计思路2.1 整体构架智能空调系统由温度检测传感器、微控制器、OLED显示屏、按键及直流电源等组件构成。传感器用于检测环境温度,通过微控制器进行处理后,将结果输出到OLED显示屏上展示。按键可根据需求调整预设阀值,切换模式等操作。2.2 硬件设计(1)温度检测传感器选择DS18B20数字温度传感器作为本系统的温度检测器件。该传感器具有精度高,响应速度快等特点,可以满足该系统的检测需求。(2)微控制器使用STM32F103系列的微控制器,在该控制器活跃的生态环境下,以及其先进的处理能力,可以对信号进行快速采集、处理和控制。(3)OLED显示屏本系统使用的是一块128 * 64 OLED显示屏,显示屏具有高亮度、高对比度和低功耗等优点,易于与STM32微控制器进行通信。2.3 软件设计在软件设计方面,实现了温度检测传感器数据的采集,使用处理算法对数据进行处理,根据预设阀值自动调节温度,同时可以根据用户需求,切换制冷、制热和关闭等3种模式。最后,将结果通过OLED显示屏进行输出。三、代码设计3.1 DS18B20温度检测代码 #include "main.h" #include "delay.h" ​ #define GPIO_PORT_TEMP GPIOA //温度数据引脚所在的端口 #define GPIO_PIN_TEMP GPIO_Pin_0 //温度数据引脚所在的引脚编号 ​ #define RCC_PORT_TEMPP RCC_APB2Periph_GPIOA // 温度引脚所在端口时钟号 ​ void USART_SendByte( USART_TypeDef * pUSARTx, uint8_t ch ); ​ void delay_us(uint32_t us){ // 延时us微秒函数 uint8_t i; for(i=0;i<us;i++){ asm("nop"); } } ​ float get_temp(){ // 获取温度函数 uint16_t temp; uint8_t buf[2]; ​ GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_InitStruct; ​ RCC_APB2PeriphClockCmd(RCC_PORT_TEMPP,ENABLE); ​ //DATA拉低480us复位 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Pin = GPIO_PIN_TEMP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIO_PORT_TEMP , &GPIO_InitStruct); GPIO_ResetBits(GPIO_PORT_TEMP , GPIO_PIN_TEMP ); delay_us(500); GPIO_SetBits(GPIO_PORT_TEMP , GPIO_PIN_TEMP ); delay_us(60); ​ //查询DS18B20是否存在 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_PIN_TEMP; GPIO_Init(GPIO_PORT_TEMP , &GPIO_InitStruct); while (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET); ​ //通信开始 GPIO_ResetBits(GPIO_PORT_TEMP , GPIO_PIN_TEMP ); delay_us(480); GPIO_SetBits(GPIO_PORT_TEMP , GPIO_PIN_TEMP ); delay_us(60); ​ //读取温度数据 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_PIN_TEMP ; GPIO_Init(GPIO_PORT_TEMP , &GPIO_InitStruct); delay_us(10); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x01; } else{ temp &=0xfe; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x02; } else{ temp &=0xfd; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x04; } else{ temp &=0xfb; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x08; } else{ temp &=0xf7; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x10; } else{ temp &=0xef; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x20; } else{ temp &=0xdf; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x40; } else{ temp &=0xbf; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x80; } else{ temp &=0x7f; } delay_us(50); ​ //读取温度小数点数据 if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x01; } else{ buf[0] &=0xfe; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x02; } else{ buf[0] &=0xfd; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x04; } else{ buf[0] &=0xfb; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x08; } else{ buf[0] &=0xf7; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x10; } else{ buf[0] &=0xef; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x20; } else{ buf[0] &=0xdf; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x40; } else{ buf[0] &=0xbf; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x80; } else{ buf[0] &=0x7f; } delay_us(50); ​ return (float)temp+((float)buf[0]/16.0); // 将温度整数位和小数位转换为十进制 } ​ int main(void){ ​ char temp_buf[20]; // 接收温度值的临时缓冲区 ​ USART_InitTypeDef USART_InitStruct; ​ RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); ​ USART_InitStruct.USART_BaudRate = 115200; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_Init(USART1,&USART_InitStruct); ​ USART_Cmd(USART1,ENABLE); ​ while(1){ float temp_get=get_temp(); // 获取当前温度值 sprintf(temp_buf,"temp:%0.1f\r\n",temp_get); // 将温度值格式化为字符串输出 for(int i=0;i<strlen(temp_buf);i++){ // 逐字符发送温度值至串口 USART_SendByte(USART1,temp_buf[i]); } delay_ms(1000); // 延时1s后再次获取温度值 } } ​ void USART_SendByte( USART_TypeDef * pUSARTx, uint8_t ch ){ while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE) == RESET); USART_SendData(pUSARTx,ch); }3.2 OLED显示屏代码 #include "main.h" #include "delay.h" #include "oled.h" ​ void iic_init(void); void GPIO_I2C_Delay(void); void write_com(unsigned char com); void write_data(unsigned char data); ​ int main(void){ ​ unsigned char x,y; iic_init(); // 初始化IIC接口 OLED_Init(); // 初始化OLED显示屏 ​ while(1){ OLED_ShowString(0,0,"1234"); // 在OLED显示屏上显示字符串“1234” delay_ms(500); // 延时500ms OLED_Clear(); // 清空OLED显示屏 } } ​ void iic_init(void){ GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //GPIOB使能 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); //I2C1使能 ​ GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD; //配置开漏输出 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); ​ I2C_InitTypeDef I2C_InitStruct; I2C_DeInit(I2C1); ​ I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; // I2C 模式 I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; // 数传比率 2 I2C_InitStruct.I2C_OwnAddress1 = 0x00; // 地址1, 设备地址 I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; // 开启I2C应答机制 I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //设备地址长度为 7 位 I2C_InitStruct.I2C_ClockSpeed = 400000; // 时钟速度为400kHz I2C_Cmd(I2C1, ENABLE); ​ I2C_Init(I2C1, &I2C_InitStruct); } ​ void GPIO_I2C_Delay(void){ uint32_t i = 1000; while(i--); } ​ void write_com(unsigned char com){ while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY)); //等待I2C总线空闲 I2C_GenerateSTART(I2C1,ENABLE); //发送起始信号 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1,0x78,I2C_Direction_Transmitter);//选择写入模式,发送从机器OLED的地址0x78 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1,0x00); //发送控制字节0x00表示写入指令 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1,com); //写入要发送的指令 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1,ENABLE); //停止信号,传输结束 } ​ void write_data(unsigned char data){ while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY)); //等待I2C总线空闲 I2C_GenerateSTART(I2C1,ENABLE); //发送起始信号 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1,0x78,I2C_Direction_Transmitter); //选择写入模式,发送从机器OLED的地址0x78 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1,0x40); //发送控制字节0x40表示写入数据 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1,data); //写入要发送的数据 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1,ENABLE); //停止信号,传输结束 }
  • [技术干货] 基于STM32设计的太阳能热水器
    一、概述本项目使用 STM32F103C8T6 微控制器作为核心处理器,结合多个传感器和执行器,实现了太阳能热水器的自动控制。通过对光照、温度、水位等各种参数的监测和分析,对水泵、电磁阀等设备进行自动控制,从而实现太阳能热水器的高效、安全、可靠运行。二、硬件设计(1)模块组成太阳能热水器模块主要由以下几个部分组成:光敏传感器模块:用于检测阳光强度,反映太阳辐射强度和方向。温度传感器模块:用于检测太阳能集热器表面和水箱内的温度,并根据温度变化调整水泵、电磁阀等设备的运行状态。液位传感器模块:用于检测水箱内的液位,并根据液位高低控制水泵和电磁阀的启停。水泵模块:通过控制水泵的启停,实现水循环流动和充水功能。电磁阀模块:通过控制电磁阀的开关,实现热水器的放水和接水功能。(2)硬件连接其中,光敏传感器模块、温度传感器模块和液位传感器模块通过 ADC 接口与 STM32F103C8T6 微控制器进行连接;水泵模块和电磁阀模块则通过 GPIO 口控制。连接方式如下:光敏传感器模块:将光敏传感器输出口与 ADC1 通道10 连接,并用一个电位器调整 ADC 的参考电压,使其范围在 0-3.3V 之间。温度传感器模块:将 DS18B20 温度传感器数据线与 GPIOA 的 PA8 引脚连接,并将 VCC 和 GND 分别接到 3.3V 和 GND。液位传感器模块:将液位传感器输出口与 ADC1 通道11 连接,并用一个电位器调整 ADC 的参考电压。水泵模块:将水泵正极接到 GPIOB 的 PB1 引脚,将负极接到电源的负极。电磁阀模块:将电磁阀正极接到 GPIOB 的 PB0 引脚,将负极接到电源的负极。三、软件设计3.1 任务分配整个项目采用 FreeRTOS 系统进行开发,实现数数的监测和控制,开发以下几个任务:光敏传感器任务:定时读取光敏传感器输出口的电压值,并进行数据处理,得到当前的光照强度。温度传感器任务:定时向 DS18B20 温度传感器发送温度采样请求,接收并解析响应数据,得到当前的太阳能集热器表面温度和水箱内温度。液位传感器任务:定时读取液位传感器输出口的电压值,并进行数据处理,得到当前的水箱水位高度。控制任务:根据光照强度、温度和水位高度等参数,决定是否需要启动水泵或电磁阀等设备。伪代码如下: void Light_Sensor_Task(void) { while (1) { voltage = ADC_Get_Voltage(); // 获取光敏传感器输出电压 light_intensity = voltage * 100 / 3.3f; // 根据电压计算光照强度 vTaskDelay(1000); // 延时 1s } } ​ void Temperature_Sensor_Task(void) { while (1) { DS18B20_Start_Conversion(); // 向温度传感器发送采样请求 temperature1 = DS18B20_Read_Temperature(); // 读取太阳能集热器表面温度 temperature2 = DS18B20_Read_Temperature(); // 读取水箱内温度 vTaskDelay(1000); // 延时 1s } } ​ void Water_Level_Sensor_Task(void) { while (1) { voltage = ADC_Get_Voltage(); // 获取液位传感器输出电压 water_level = voltage * 100 / 3.3f; // 根据电压计算水位高度 vTaskDelay(1000); // 延时 1s } } ​ void Control_Task(void) { while (1) { if (light_intensity > THRESHOLD && temperature1 > THRESHOLD && water_level > THRESHOLD) // 如果各种参数均符合要求,则启动水泵和电磁阀 { GPIO_SetBits(GPIOB, GPIO_Pin_1); // 启动水泵 GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 关闭电磁阀 } else // 否则关闭水泵,打开电磁阀,放水 { GPIO_ResetBits(GPIOB, GPIO_Pin_1); // 关闭水泵 GPIO_SetBits(GPIOB, GPIO_Pin_0); // 启动电磁阀 } vTaskDelay(1000); // 延时 1s } }3.2 光敏传感器任务 /* 光敏传感器任务 */ void Light_Sensor_Task(void *pvParameters) { uint16_t adc_value; ​ while (1) { /* 读取 ADC 值并计算光照强度 */ if (HAL_ADC_Start(&hadc1) == HAL_OK) { if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { adc_value = HAL_ADC_GetValue(&hadc1); light_intensity = adc_value * 3300 / 4096.0; } } ​ vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1s } }在函数中,声明一个变量 adc_value 用于存储读取到的 ADC 值。使用 if 条件语句检查 ADC 是否成功启动,并且使用 HAL_ADC_PollForConversion() 函数判断当前转换是否完成,如果转换完成,就获取 ADC 值,并且通过简单的计算公式将 ADC 值转换为光照强度值,最后将结果存储在 light_intensity 变量中。3.3 温度传感器任务 /* 温度传感器任务 */ void Temperature_Sensor_Task(void *pvParameters) { float temperature; ​ /* 初始化 DS18B20 */ DS18B20_Init(&htim2, GPIOA, GPIO_PIN_10); ​ while (1) { /* 读取温度值 */ temperature = DS18B20_Read_Temperature(); ​ /* 将读取到的温度值存储在全局变量中 */ current_temperature = temperature; ​ vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1s } }在函数中,声明一个变量 temperature 用于存储读取到的温度值。然后,调用函数 DS18B20_Init() 初始化 DS18B20 温度传感器。使用 DS18B20_Read_Temperature() 函数读取温度值,并且将结果存储在 temperature 变量中。最后,将读取到的温度值存储在全局变量 current_temperature 中。3.4 液位传感器任务 /* 液位传感器任务 */ void Liquid_Level_Sensor_Task(void *pvParameters) { uint16_t adc_value; float voltage; ​ /* 初始化液位传感器 GPIO 口 */ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET); ​ while (1) { /* 读取 ADC 值并计算电压值 */ if (HAL_ADC_Start(&hadc1) == HAL_OK) { if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { adc_value = HAL_ADC_GetValue(&hadc1); voltage = adc_value * 3.3 / 4096.0; } } ​ /* 根据电压值计算液位高度 */ if (voltage < 0.5) { liquid_level = 0.0; } else if (voltage > 2.5) { liquid_level = 100.0; } else { liquid_level = (voltage - 0.5) * 100.0 / 2.0; } ​ vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1s } }在函数中,声明变量 adc_value 和 voltage,分别用于存储读取到的 ADC 值和计算得到的电压值。使用 HAL_GPIO_WritePin() 函数初始化液位传感器 GPIO 口,将启用传感器的引脚设置为高电平。使用 if 条件语句检查 ADC 是否成功启动,并且使用 HAL_ADC_PollForConversion() 函数判断当前转换是否完成,如果转换完成,就获取 ADC 值,并且通过简单的计算公式将 ADC 值转换为电压值,并将结果存储在 voltage 变量中。由于需要使用电压值计算液位高度,使用 if 条件语句检查电压是否小于低液位警戒电压 0.5V 或者大于高液位警戒电压 2.5V,如果是则分别将液位高度设置为 0% 或 100%,否则使用简单的线性关系计算液位高度。3.5 控制任务 /* 控制任务 */ void Control_Task(void *pvParameters) { float temperature_setpoint = 25.0; // 设定温度值 float liquid_level_setpoint = 50.0; // 设定液位高度值 float temperature_error, liquid_level_error; float temperature_integral, liquid_level_integral; float temperature_derivative, liquid_level_derivative; float temperature_output, liquid_level_output; ​ float kp_temperature = 0.5, ki_temperature = 0.1, kd_temperature = 0.05; // 温度 PID 参数 float kp_liquid_level = 0.2, ki_liquid_level = 0.05, kd_liquid_level = 0.02; // 液位高度 PID 参数 ​ while (1) { /* 计算温度 PID 控制器输出 */ temperature_error = temperature_setpoint - current_temperature; temperature_integral += temperature_error; temperature_derivative = temperature_error - last_temperature_error; temperature_output = kp_temperature * temperature_error + ki_temperature * temperature_integral + kd_temperature * temperature_derivative; last_temperature_error = temperature_error; ​ /* 计算液位高度 PID 控制器输出 */ liquid_level_error = liquid_level_setpoint - liquid_level; liquid_level_integral += liquid_level_error; liquid_level_derivative = liquid_level_error - last_liquid_level_error; liquid_level_output = kp_liquid_level * liquid_level_error + ki_liquid_level * liquid_level_integral + kd_liquid_level * liquid_level_derivative; last_liquid_level_error = liquid_level_error; ​ /* 通过 PWM 控制加热器和水泵电机 */ if (temperature_output > 0.0) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, (uint16_t)(temperature_output * 1000)); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0); } ​ if (liquid_level_output > 0.0) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, (uint16_t)(liquid_level_output * 1000)); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); } ​ vTaskDelay(pdMS_TO_TICKS(10)); // 延时 10ms } }在函数中:(1)定义参数和变量,包括设定温度值、设定液位高度值、温度 PID 控制器的参数、液位高度 PID 控制器的参数等。使用 while 循环处理控制逻辑,循环开始时,计算温度 PID 控制器输出。(2)计算当前误差,并将误差累积到积分项中。计算误差变化率,并使用 PID 参数计算出输出值,将结果存储在 temperature_output 中,并将当前误差存储在 last_temperature_error 中以便于下一次计算,计算液位高度 PID 控制器输出。(3)根据控制器输出值通过 PWM 控制加热器和水泵电机的运行状态。如果输出值大于 0,则启用电机或加热器并设置对应的 PWM 占空比,否则关闭电机或加热器并将 PWM 占空比设为 0。
  • [技术干货] 基于STM32设计的数显热水器
    一、项目介绍当前介绍的项目是基于 STM32F103ZET6 系列 MCU 设计的数显热水器,通过显示屏来显示热水器的温度及其工作状态,通过 PT100 传感器来检测热水器的温度变化,并通过电加热片实现加热过程,以达到控制热水器温度的目的。二、设计流程2.1 硬件选型STM32F103ZET6 系列 MCUOLED 显示屏PT100 温度传感器电加热片继电器2.2 软件设计(1)显示屏使用 OLED 显示屏来显示热水器的温度及其工作状态,通过 SPI 接口与 STM32 芯片进行通讯。设计温度值及其单位、热水器工作状态等。(2)温度传感器使用 PT100 温度传感器来检测热水器内部温度的变化,并将数据通过 ADC 转换后,传输给 STM32 芯片,以实现对热水器加热过程的控制。(3)电加热片使用电加热片模拟热水器加热过程,通过继电器控制电加热片的通断,以调节热水器的温度。(4)控制系统通过 STM32 芯片来实现对热水器的控制,读取温度传感器的数据。三、代码设计3.1 OLED显示屏(1)SPI 接口初始化需要对 STM32F103ZET6 的 SPI 接口进行初始化配置,设置相关的时钟和模式,使其能够与 OLED 显示屏进行通讯。 RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI3, ENABLE); // 打开SPI3时钟 SPI_InitTypeDef spi_init_type; spi_init_type.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spi_init_type.SPI_Mode = SPI_Mode_Master; spi_init_type.SPI_DataSize = SPI_DataSize_8b; spi_init_type.SPI_CPOL = SPI_CPOL_Low; spi_init_type.SPI_CPHA = SPI_CPHA_1Edge; spi_init_type.SPI_NSS = SPI_NSS_Soft; spi_init_type.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; // 设置 SPI 时钟频率为 72 MHz / 32 = 2.25MHz spi_init_type.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI3, &spi_init_type); SPI_Cmd(SPI3, ENABLE);(2)OLED 显示屏初始化以下是 OLED 显示屏的初始化代码: void OLED_Init(void) { GPIO_SetBits(GPIOB, GPIO_Pin_6); //RST SET GPIO_ResetBits(GPIOB, GPIO_Pin_6); //RST RESET GPIO_SetBits(GPIOB, GPIO_Pin_6); //RST SET ​ write_command(0xAE); // 关闭显示 write_command(0xD5); // 设置时钟分频因子,震荡频率 write_command(0x80); // 分频因子=1 ,震荡频率(fosc)=8MHz write_command(0xA8); // 设置驱动路数:MUX(复用方式) write_command(0x1F); // 1/32 duty (0x0F~0x3F) write_command(0xD3); // 设置显示偏移 write_command(0x00); // 不偏移 write_command(0x40); // 设置显示开始行[5:0], 对于设置了32行的液晶, // 这里的值为0表示从0行开始显示 write_command(0x8D); // 对比度设置 write_command(0x14); // AHB参考电压256等分 移位[3:0]100[n,1/256] write_command(0x20); // 水平方向上的寻址模式 write_command(0x00); // 垂直方向上的寻址模式 write_command(0xA1); // 设置段再映射 write_command(0xC0); // 设置COM扫描方向 write_command(0xDA); // 设置COM引脚硬件配置 write_command(0x12); write_command(0x81); // 对比度设置 write_command(0xBF); // 设置电荷泵电压 write_command(0xD9); // 设置预充电周期 write_command(0xF1); write_command(0xDB); // 设置VCOMH电压倍率 write_command(0x40); write_command(0xAF); // 打开显示 ​ OLED_Clear(); // 清屏 }(3)OLED 显示函数接下来编写 OLED 显示函数,实现字符和数字的显示功能。 void OLED_show_string(uint8_t x, uint8_t y, char *str) { uint8_t i = 0; while (str[i] != '\0') { OLED_show_char(x, y + i * 8, str[i]); ++i; } } ​ void OLED_show_char(uint8_t x, uint8_t y, char ch) { uint8_t c = ch - 32; if (c >= 96) return; uint8_t* buffer = (uint8_t*)oled_buffer; uint8_t cx, cy; for (cy = 0; cy < 8; cy++) { uint8_t line = font[c][cy]; for (cx = 0; cx < 6; cx++) { if (line & 0x1) { buffer[(y + cy) * OLEDWIDTH + x + cx] = 1; } else { buffer[(y + cy) * OLEDWIDTH + x + cx] = 0; } line >>= 1; } } OLED_Draw_Pixel(x + 6, y, 0); OLED_Draw_Pixel(x + 6, y + 1, 0); OLED_Draw_Pixel(x + 6, y + 6, 0); OLED_Draw_Pixel(x + 6, y + 7, 0); }(4)结果显示在代码中调用 OLED_show_string 函数和 OLED_show_char 函数显示数值和字符。 OLED_Init(); OLED_Clear(); OLED_show_string(0, 0, "HELLO WORLD!"); OLED_show_string(0, 16, "TEMP:20 C");3.2 测温代码(1)引脚配置需要对 STM32F103ZET6 的 IO 口进行配置,将用于连接 PT100 温度传感器的引脚设置为输入模式。这里以 PA0 引脚作为 PT100 传感器的连接口(即 PT100 三线连接中的 R3 端),代码如下: GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式 GPIO_Init(GPIOA, &GPIO_InitStructure);(2)ADC 配置接下来需要对 STM32F103ZET6 的 ADC 进行初始化配置,使其能够读取 PT100 温度传感器输出的电压信号。这里以 ADC1 通道5 作为读取口,代码如下: ADC_InitTypeDef ADC_InitStructure; RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 设置 ADC 时钟为 PCLK2 的 1/6 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 打开 ADC1 时钟 ADC_DeInit(ADC1); // 初始化 ADC1 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); // 开启 ADC1(3)温度转换函数根据 PT100 温度传感器输出电压与温度的关系,可使用线性函数计算出温度值。转换公式如下: Rt = (Vref - Vpt) / Ipt // Rt 为 PT100 的阻值,Vref 为基准电压,Vpt 为 PT100 输出电压,Ipt 为 PT100 驱动电流 Temp = a * Rt + b // Temp 为温度值,a 和 b 为经过拟合后的系数其中 Rt 的计算需要使用差分运算放大器进行转换,这里不再赘述。假设已经得到 Rt 值,则温度转换函数代码如下: float PT100_Get_Temperature(float Rt) { float a = 3.9083e-3f, b = -5.775e-7f, R0 = 100.0f; // 根据实际数据进行拟合得到 a、b 和 R0 的值 float Tem, delta; delta = pow(Rt / R0, 2) + a * (Rt / R0) + b; Tem = (delta > 0) ? (-R0*a + sqrt(delta)) / (2 * b) : 0; return Tem; }(4)数据采集根据差分放大器输出的电压值得到 PT100 温度传感器的阻值,再根据阻值计算出实际温度,最后将温度值通过串口打印出来。以下是数据采集代码:float ADC_Get_Voltage(void){ float voltage = 0; uint16_t adc_val = 0; ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_239Cycles5); // 配置 ADC 通道5 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 使能软件触发 ADC 转换 while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换结束 adc_val = ADC_GetConversionValue(ADC1); // 读取 ADC 转换结果 voltage = (float)adc_val * 3.3f / 4096; // 计算基准电压 return voltage;}float PT100_Get_Rt(float Vpt){ float Rsource = 10e3f, Rpt = 100.0f; // Rsource 为差分放大器输出电阻,Rpt 为 PT100 阻值 float Ipt = (3.3f - Vpt) / Rsource; // 计算 PT100 驱动电流 float Rt = (3.3f - Vpt) / Ipt; // 根据欧姆定律计算出 PT100 阻值 return Rt;}void USART1_Send_Float(float f){ char buf[32]; sprintf(buf, "%.1f\r\n", f); // 转换为字符串 while (*buf) { USART_SendData(USART1, *buf); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); buf++; }}int main(void){ ... while (1) { float Vpt = ADC_Get_Voltage(); // 获取差分放大器输出电压 float Rt = PT100_Get_Rt(Vpt); // 计算 PT100 阻值 float Temp = PT100_Get_Temperature(Rt); // 根据阻值计算温度 USART1_Send_Float(Temp); // 将温度值打印到串口 delay_ms(500); } ...}
总条数:501 到第
上滑加载中