我的STM32为什么会突然卡死?由于没有进行重定向而导致的LCD卡死。串口和屏幕奇怪的联动
最近有同学像我请教他的LCD屏幕无法使用。首先的现象是屏幕已经亮了,但是无法显示字符或者其他操作。 看似是没有点亮屏幕, 其实这是一个非常关键的现象 !屏幕的这个亮度,其实已经说明了LCD被驱动成功了!肯定是因为其他的原因导致程序死机了。 而我收到这个屏幕的时候,仅有STlink用来供电和下载(其实我拿到的时候已经猜到是什么问题了) 于是我打开他发给我的驱动。 可以看到,驱动非常完备,编程习惯优秀且美观,可以肯定这是一款移植性非常好的驱动。 并且其初始化函数中,也会根据LCD ID的不同对不同设备进行不同的初始化操作。 可以说这份代码做的非常的优秀。 那么朋友们看到这里还没有意识到问题嘛? 没错,其初始化函数中调用printf函数,而这个函数必须依赖于串口重定向还需要打开魔术棒中的特殊设置。 不然就会导致整个程序出现死机。 所以最好的解决办法就是去掉printf函数或者正确的配置串口并进行初始化。 可以看到,正确的初始化之后我们可以显示我们的字符,所以破案,由于没有进行串口重定向而导致的串口卡死的问题。 其实这里还有一个问题。 按理来说画笔的默认颜色是红色,但是我发现我写的时候没有用,然后我就把画笔的颜色改为了黑色,发现就可以用了,就非常的奇怪。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 */
/* 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_FSMC_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
TFTLCD_Init();
POINT_COLOR = BLACK;
LCD_ShowString(10,10,300,100,24,"Hello World");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
所以大家在使用STM32的时候,出现了许多问题实际上是有迹可循的,非常考验大家的编程功底。 例如当我LCD接上电发现可以亮的时候。其实我就已经明白了估计并不是LCD的问题,而当我看到没有使用串口的时候就已经能大致猜到和串口卡死有关系了。 所以如果我们习惯性的使用LED的闪烁来判断程序有没有卡死,就会明白我们的程序有没有卡死。 所以优秀的编程习惯可以节省我们大量的时间,帮我更好的完成我们的任务。 而这就需要我们平时的经验积累,例如知道这种库函数会使用printf调用,串口重定向没处理好会导致程序死机,屏幕白色说明初始化已经成功了(不成功大部分就是黑色)出现了雪花屏幕和白色就说明是已经成功了的,不过白色大部分的原因都是屏幕大小没有设置好。
Multisim的滤波器幅频特性与相频特性曲线获取
幅频特性曲线是描述系统频率响应的幅度随频率变化的曲线。具体来说,它展示了在不同频率下,系统对于输入信号的放大或衰减程度。在幅频特性曲线中,幅度大的地方对应通带,意味着这些频率成分通过系统时衰减较小;而幅度小的地方对应阻带,表示这些频率成分通过系统时衰减较大。 幅频特性曲线通常用于评估系统的性能,特别是在滤波器设计、自动化控制、音频处理以及通信等领域。例如,在滤波器设计中,通过观察幅频特性曲线,我们可以了解滤波器对不同频率成分的响应情况,从而判断其是否符合设计要求。在自动化控制领域,幅频特性曲线可用于评估控制器设计的效果,帮助工程师优化系统性能。 绘制幅频特性曲线时,通常以频率为横坐标,以幅度为纵坐标,描绘出幅度随频率变化的曲线。在绘制过程中,需要正确设置测量参数,如频率范围、采样率和分辨率等,并对测量数据进行处理和分析,如滤波、插值和曲线拟合等。 总的来说,幅频特性曲线是理解和分析系统性能的重要工具,尤其在处理涉及多频率成分的系统时,其应用更为广泛。 我们在之前有介绍过如何使用ADI滤波器设计向导设计符合要求的滤波器,今天我们将介绍如何使用Multisim14获取幅频特性曲线。 首先我们利用ADI滤波器设计向导,设计一款通带100HZ,截止频率2KHZ的滤波器,其通带增益为2DB,截止频率为-40DB。 DB主要作为功率增益的单位,用来表示一个相对值。它通常用来衡量放大器的性能,即信号通过放大器后的输出功率与输入功率之比。 在电子领域中,dB与功率或电压、电流之间的关系可以通过以下公式表示:对于功率,dB = 10lg(A/B);对于电压或电流,dB = 20lg(A/B)。其中,A和B代表参与比较的功率值或者电流、电压值。 我们设置搭建好上图电路,这里需要一个V1信号源作为激励源(V1)多少参数实际上无关。 这里如果有朋友线上没有显示网络标签的话可以在设置中修改。 搭建好电路后,我们有两种办法获取电路的幅频特性曲线。 波特测试仪 这里我们放置 波特测试仪,他有四个端口,我们将IN+接入信号激励源,OUT+接入输出端。 我们的频率设置为1HZ~4KHZ,幅度显示从5~-90DB 可以看到,在通频带内,增益约为1.966DB接近2DB,ADI设计工具和Multisim仿真接近。 2KHZ的时候,也是接近-50db,仿真和设计工具的非常接近。 交流扫描 第二种方法则是ACsweep,我们在simulate中打开分析,选择交流扫频 这里需要提前注意下,我的激励源的网络标签是5,输出端的网络标签是1. 设置好频率范围,输出刻度选择对数。 编辑表达式,我们选择输出电压/输入电压(V1/V5)。之后像正常工程一样启动(Run)我们的工程。 得到曲线之后开启光标。 上面的图是他的幅频特性曲线,下面的图是他的相频特性曲线。 幅频特性曲线是分析电路性能的重要工具。通过观察曲线的变化趋势,用户可以了解电路在不同频率下的增益、相位变化以及可能的谐振或滤波特性。这些信息对于优化电路设计、提高电路性能以及预测电路在实际应用中的表现具有重要意义。 此外,Multisim还提供了丰富的后处理功能,允许用户对仿真结果进行进一步的分析和处理。例如,用户可以对幅频特性曲线进行平滑处理、提取关键参数或与其他仿真结果进行比较等。
STM32CubeMX输入捕获测周法检测频率
STM32统计频率有许多种方式,有使用外部中断+定时器的方法,但是还有一种更加准确,可以计算占空比的方法。 即使用输入捕获来统计。 输入捕获的基本原理是通过检测TIMx_CHx上的边沿信号(如上升沿或下降沿),在边沿信号发生跳变时,将当前定时器的值(TIMx_CNT)存放到对应的通道的捕获/比较寄存器(TIMx_CCRx)中,从而完成一次捕获。此外,还可以配置捕获时是否触发中断或DMA等。 我们可以配置一个外部中断的中断回调函数,在中断回调函数中,我们可以计算两个脉冲的时间差来准确的计算脉冲的周期(频率) 这里我使用蓝桥杯的竞赛平台,STM32G431原理图中有两个NE555产生方波连接至PA15和PB4。 这里的PB4和PA15分别对应了TIM16的CHANNLE1和TIM8的CHANNLE1。 触发方式选择上升沿触发,这边定时器分频系数选择150-1,1us计数一次,最高计数65535即计数时间65.53ms,对应的最低频率为15HZ。 最高计数频率为1us即100KHZ,滤波系数选择0(因为不是按键没有杂波) 开启定时器中断。 这里的中断触发有两种可能:第一是计数器溢出导致的中断触发,第二种是检测到上升下降沿导致的中断触发。 由于我们的测量频率在我们的计数频率之内,所以我们不考虑计数溢出导致的中断触发。 这里分频系数也不能太高,我就干脆不分频了。 因为等会我们会清除计数器struct Pre
{
float HighTime;//高电平时间
float LowTime;//低电平时间
uint8_t CapStatus;//计算是否是第二次捕获
float Frequent;//频率值
float Time;
uint8_t EndFlag;//捕获结束
float Duty;//占空比
};
void GetFreq(struct Pre*pre,TIM_HandleTypeDef * htim,uint32_t Channel)
{
if(!pre->EndFlag)
{
if(pre->CapStatus == 0) //第一个上升沿
{
pre->CapStatus = 1;
__HAL_TIM_SET_CAPTUREPOLARITY(htim, Channel, TIM_INPUTCHANNELPOLARITY_FALLING); //设置成下降沿触发
__HAL_TIM_SetCounter(htim, 0); //清空定时器计数值
pre->HighTime = HAL_TIM_ReadCapturedValue(htim, Channel); //由第一个上升沿设为起始位置
}else if(pre->CapStatus == 1) //第一个下降沿
{
pre->CapStatus = 2;
pre->LowTime = HAL_TIM_ReadCapturedValue(htim, Channel); //低电平起始位置
__HAL_TIM_SET_CAPTUREPOLARITY(htim, Channel,TIM_INPUTCHANNELPOLARITY_RISING); //设置成上升沿触发
}else if(pre->CapStatus == 2) //第二个上升沿
{
pre->CapStatus = 0;
pre->HighTime = HAL_TIM_ReadCapturedValue(htim, Channel);
//计算频率
pre->Frequent = 1/pre->HighTime*100000;
//计算占空比
pre->Duty = (float)pre->LowTime / (pre->HighTime+1);
pre->EndFlag = 1;
}
}
}
我们定义一个结构体存放计数值和频率值,之后写一个函数来计算频率值。 主要统计两个上升沿之间的时间。 HAL_TIM_Base_Start_IT(&htim16);
HAL_TIM_IC_Start_IT(&htim16,TIM_CHANNEL_1);
__HAL_TIM_ENABLE_IT(&htim16,TIM_IT_UPDATE); //一定要开启TIM16的更新中断
HAL_TIM_Base_Start_IT(&htim8);
HAL_TIM_IC_Start_IT(&htim8,TIM_CHANNEL_1);
__HAL_TIM_ENABLE_IT(&htim8,TIM_IT_UPDATE); //一定要开启TIM8的更新中断
开启定时器计数以及使能中断。void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim==&htim8)
{
GetFreq(&pre1,htim,TIM_CHANNEL_1);
}
}
输入捕获到频率检测。 if(pre1.EndFlag)
{
sprintf(pr,"FreQuent:%.2f",pre1.Frequent);
LCD_DisplayStringLine(Line4,pr);
pre1.EndFlag = 0;
}
如果检测结束则打印频率值。 我们可以看到理论最低频率为716.41HZ
第十五届蓝桥杯嵌入式赛道竞赛平台STM32G431相关代码基于CubeMX的整合
最近第十五届蓝桥杯也即将开始比赛了,我这边也收到了蓝桥杯官方提供的竞赛平台(虽然要还回去可恶真小气) 其板载DAP下载器,两路电压采集,两路频率输出,这里的频率输出由555定时器提供以及按钮和部分引出IO。 从官网中下载了嵌入式赛道的资料。 首先最最关心的还是屏幕问题。 资料中提供了HAL库和LL库的驱动,标准库玩家直呼退赛,但是没有提供相关CubeMX工程。 因此本期我们将实现CubeMX的配置与各功能的移植。 首先我们在CubeMX中找到STM32G431R8T6,找到我们的板子型号。 由于原理图中 没有看到相关的低速晶振 ,那么在RCC配置中我们只需要选择高速晶振旁路时钟源。 并且由于原理图中,驱动LCD利用了并口操作,使用了整组PC端口进行数据的写入,因此我们需要将相关的IO口全部配置为输出,并且将所有可配置端口设置为高速模式。 在LCD_Init函数中,我们可以将IO初始化的内容注释掉,这部分在CubeMX中已经完成了初始化。 LCD_Init();//LCD初始化
HAL_Delay(100);//等待初始化成功
LCD_SetBackColor(Black);//设置背景为黑色
LCD_SetTextColor(White);//设置字体颜色为白色
LCD_Clear(Black);//黑色填充
HAL_Delay(100);//等待
LCD_DisplayStringLine(Line4, (unsigned char *)" Hello,world. ");//输出Hello World
HAL_Delay(1000);//等待
在While前输入以上代码,我们即可实现在LCD中打印黑底白字的Hello World. 在原理图中我们可以看到其由M24C02以及MCP4017两个I2C设备 M24C02是一款EEPROM存储器,即电可擦可编程只读存储器。它采用串行I²C接口,存储容量为2Kbit,组织为256 x 8位。其工作电压范围为2.5~5.5V,最大时钟频率为400kHz。M24C02支持总线控制,通过驱动地址可以完成设备的选定及写入/读取控制。该芯片具有字节写入和片写入两种写入模式,可以方便地向EEPROM中写入数据。 MCP4017是一款可编程电阻,它内置了7位寄存器,提供了多种电阻调节功能。 但是其IO PB6/7并没有硬件I2C的功能,因此我们需要用软件模拟I2C。 将PB6和PB7设置为输出模式,这里需要开漏输出,推挽输出会导致IO烧毁,具体可以看看之前的文章,关于开漏和推挽输出的区别。 详细解析STM32中GPIO有四种模式 这里也不需要上拉电阻,因为原理图中板载上拉电阻了。 启动电平需要设置为高电平,高电平代表I2C设备空闲状态。 还有需要注意的是,GPIO设置为开漏输出是可以读取总线电平的,这个细节可能很多人不知道。 我们编程完我们的24C02的代码。 char s[] = {'1','2','3','4','5'};
char Data[5] = {‘0’};
定义一组待写入的数据和待会存放数据的缓存区。 for(unsigned char i = 1;i[removed]
小白也会的三极管电路搭建(1)基本放大电路的实现
三极管,全称应为半导体三极管,也称双极型晶体管、晶体三极管,是一种控制电流的半导体器件。其作用是把微弱信号放大成幅度值较大的电信号,也用作无触点开关。三极管是半导体基本元器件之一,具有电流放大作用,是电子电路的核心元件。三极管是在一块半导体基片上制作两个相距很近的PN结,两个PN结把整块半导体分成三部分,中间部分是基区,两侧部分是发射区和集电区,排列方式有PNP和NPN两种。 三极管具有三个电极:发射极(e)、基极 (b)和集电极(c)。它的基本功能可以概括为:当基极电流(或电压)发生微小变化时,集电极电流将发生较大的变化,这是三极管的电流放大效应。三极管可以放大直流信号和交流信号,这是由三极管的特性决定的。 根据三极管工作状态的差异,可将其分为截止状态、放大状态和饱和状态。在截止状态下,三极管不导通;在放大状态下,三极管具有电流放大作用;在饱和状态下,三极管处于深度导通状态。 本期我们将使用三极管进行信号放大。 我们将选用典型的NPN三极管,仿真平台为手机端仿真软件。 可以看到,在放大状态下(集电极反偏,发射极正偏)时,集电极的电流是基极电流的100倍(视具体的型号有所不同),这个放大倍数通常是不会变的(会受温度影响) 这个倍数被称为放大倍数。 而怎么判断放大倍数呢?简单的判断方法就是集电极反偏,发射级正偏。例如NPN三极管可以看作两个N级夹着基极。 此时如果Vc的电压比Vb电压要大,而Vb-Ve的电压约为0.8V(二极管导通电压)则是发射极正偏,集电极反偏。 利用这个特性,我们可以计算出三极管的参数。 如图,已知二极管导通压降为0.8V左右,可以得到通过三极管的电流约为0.2ma,已知放大倍数是100倍,因此集电极电流为20mA左右,那么流过电阻所导致的压降约为2V,所以集电极的电位约为10V,满足发射极正偏,集电极反偏的条件。 利用三极管可以放大电流的特性,我们可以使用三极管完成信号的放大。 当基极电流增加的时候,导致集电极电流增加,从而减小集电极电压,实现一个方向放大器的目的。 不过由于这是一个单电源电路,因此我们无法放大正弦波的负板部分。 因此我们需要为这个电路添加一个偏置,让交流信号可以正常的工作。 如图构成了几个很基本的共集电极放大器。 根据如图三条性质,我们即可算出电路的静态工作点以及放大倍数等信息。图中有误(第三条应该是12V-Ui-0.8V导通压降)。 例如当Ui为0时,希望Uo的输出是在6V,结合Ui的范围确定放大倍数,这样子就可以计算静态工作点和电阻的大小的选择。 但是这个电路也是有明显的缺点的。由于晶体管工作时,其放大倍数通常受到温度的影响从而抬高静态工作点。 因此在这个电路的基础上,我们在发射机加上了一个电阻,对其进行负反馈。 由于这个发射极的反馈电阻存在,当放大倍数变大时导致流经发射极的电流增大导致发射极电压升高,从而导致基极电压升高(二者在导通状态下总有一个相差不大的压降),进而导致基极电流减小,从而减小集电极电流,使得电压得到修正。 但是如图可见,这样子的设计也有一个非常大的弊端,就是会极大的影响放大倍数。因此我们需要利用直流信号和交流信号分开处理的思想。 我们为反馈电阻添加一个旁路电容,这个旁路电容的存在可以为交流信号提供一个阻抗很小的对地通道,而对于直流信号而言则是开路。 这个电路就可以很好的抵抗温度对于直流工作点的干扰以及保证放大倍数并不会受到太多的影响。 这里着重强调一下旁路电容,许多朋友会分不清退耦电容电容和旁路电容,可是如果从为交流信号提供旁路的角度,这个叫法是非常贴切的。 下期介绍更加复杂的放大电路解析以及三极管等效电路的原理,而不是老师上课冷冰冰的一句:就是这样子的
远程LCR云测量仪的制作和成品展示
以前的文章间接的出了好几期关于555定时器测量电容,电感,电阻的方法。本期我们将制作一款其整合版,利用555定时器分别测量电阻,电感,电容的电路,并且利用ESP32上传至百度云的物联网,利用物可视快速在网页上显示。一种基于三点式振荡电路的电感测量仪/ESP32无线LCR测量仪——硬件篇电容测量之NE555这里我们利用ESP32的外部计数器配合定时器准确的采集频率,利用公式计算出实际的电容/电阻/电感的值。不过由于公式计算太过于理想,因此我们测量多组数据,利用Matlab进行函数拟合。 例如电容的测量所得频率和电容值之间的曲线就非常完美,0.9998的R,如此,我们可以将RCL的频率和电阻利用函数拟合一一对应。可以给大家看看效果,需要注意的是,这里的电容值和电感值由于博主手上并没有电桥,因此无法测量其实际值,只能利用标称来进行拟合,而这种电容,本身既有10%左右的误差,所以实际使用过程前,需利用数字电桥等精确的测量电容和电感的值来对电容和电感的计算函数进行矫正。 原理图如上,具体可以看之前的文章查看其原理。关于ESP32的代码不做过多,需要注意的是这里采用的是MQTT协议进行通讯,并且使用的是百度云的物可视技术。如果有哪里不明白的朋友可以加QQ群交流~~#include [removed]
#include [removed]
#include [removed]
// WiFi设置
const char* ssid = "***************";
const char* password = "***************";
// MQTT服务器设置
const char* mqtt_server = "*********************";
const int mqtt_port = 1883;
const char* mqtt_client_id = "ESP32_1";
const char* mqtt_username = "**************************"; // MQTT服务器需要用户名
const char* mqtt_password = "***********************"; // MQTT服务器需要密码
WiFiClient espClient;
PubSubClient client(espClient);
const int PULSE_PIN = 19; // ESP32的D19引脚
volatile long pulseCount = 0; // 脉冲计数器,volatile关键字确保在多线程环境中正确访问
// 定义按键引脚和中断编号
#define BUTTON_PIN_33 33
#define BUTTON_PIN_32 32
#define BUTTON_PIN_25 25
// 定义中断服务程序(ISR)的函数原型
void IRAM_ATTR onButtonPress33();
void IRAM_ATTR onButtonPress32();
void IRAM_ATTR onButtonPress25();
// 按键状态变量,用于记录按键是否被按下
int flag;
void setup_wifi() {
delay(10);
// 连接到WiFi网络
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected.");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
}
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
if (client.connect(mqtt_client_id, mqtt_username, mqtt_password)) {
Serial.println("connected");
// Once connected, subscribe to all topics
client.subscribe("Value");
} else {
delay(5000);
}
}
}
void setup() {
char buffer[50];
Serial.begin(115200); // 初始化串口通信
// 配置按键引脚为输入模式,并启用内部上拉电阻
pinMode(BUTTON_PIN_33, INPUT_PULLUP);
pinMode(BUTTON_PIN_32, INPUT_PULLUP);
pinMode(BUTTON_PIN_25, INPUT_PULLUP);
// 设置中断,下降沿触发
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN_33), onButtonPress33, FALLING);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN_32), onButtonPress32, FALLING);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN_25), onButtonPress25, FALLING);
pinMode(PULSE_PIN, INPUT); // 设置D19引脚为输入模式,并启用内部上拉电阻
pinMode(26, OUTPUT); // 设置D26引脚为输出模式
pinMode(27, OUTPUT); // 设置D27引脚为输出模式
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
sprintf(buffer, "{\"Type\":0,\"Va\":%lf}", 0); // %f是double的格式化占位符
if (client.connected()) {
client.publish("Value", buffer);
}
attachInterrupt(digitalPinToInterrupt(PULSE_PIN), countPulses, CHANGE); // 设置中断,检测电平变化
}
float t;
void loop() {
delay(1000); // 每秒更新一次
String str;
char buffer[50];
//Serial.println(pulseCount); // 打印脉冲数
if(flag == 3)
{
//Serial.println(fC(pulseCount));
// 发布消息到MQTT服务器
// 足够大的缓冲区来存储转换后的字符串
sprintf(buffer, "{\"Type\":3,\"Va\":%lf}", fC(pulseCount)); // %f是double的格式化占位符
if (client.connected()) {
client.publish("Value", buffer);
}
flag = 0;
}
if(flag == 2)
{
sprintf(buffer, "{\"Type\":2,\"Va\":%lf}", fL(pulseCount)); // %f是double的格式化占位符
if(fL(pulseCount)< 3000000)
{
if (client.connected()) {
client.publish("Value", buffer);
}
}
flag = 0;
}
if(flag == 1)
{
sprintf(buffer, "{\"Type\":1,\"Va\":%lf}", fR(pulseCount)); // %f是double的格式化占位符
if(fR(pulseCount)[removed]
C/C++ :快来看看使用头文件过程中可能会遇见的大坑和千字避坑指南!
主要的问题呢重定义:multiply defined重定义。 下面呢我就阐述几个头文件编写中常常会出现的几个坑!#define __2_H__extern int a;
#endif //
这时候我们编译这个工程是并不会报错的。#include "2.h"
void Change1A()
{
a = 15;
}
并且其各个文件也可以调用这个a,这个a在其头文件和各文件中是通用的。可以看到,输出的结果分别是0,30,15.可以表示这里的a是通用的被连接到了一起。但是使用这个方法也需要注意,可以有很多个extern修饰的变量,但是实际非extern修饰的变量的只能有一个。但是但是,这里其实也可以调用在某个.c文件中定义一个static修饰的变量。实际上发现static修饰的变量和头文件的变量会冲突,这个的冲突不是编译冲突!按理来说,我调用的a是extern的a,然后Change1A这个函数改变的a,经过测试之后发现是static修饰的a,也许是就近原则(可能),这样子导致我调用Change1A的时候,main中定义的a的并没有发生变化。这可以证明在.c文件中,先使用的是static修饰的变量。总结其实无论是什么方法,我们都应该避免头文件被重复定义,同名变量不应该出现在代码中,优秀的代码编辑习惯可以极大的提高我们的工作效率和容错率。这无论是给其他人阅读代码还是我们自己阅读我们的代码都带来了便利。无论是extern和static,最好的方法还是遵守编程规范,少数的巧妙运用但是不能靠这种方式来填坑Bug。
STM32上的DAC输出(三角波/正弦波/FM调制信号)并且利用ADC+DMA采样显示
DAC,即Digital to Analog Convertor,是数字到模拟转换器,也称为D/A转换器。其核心部分由R-2R电阻网络(也称倒T型电阻网络)、模拟开关和运算放大器组成。它可以将二进制码或BCD码表示的数字量转换为与其成正比的模拟量输出。DAC的工作原理主要包括数字信号采样、量化、编码和模拟信号输出几个步骤。首先,将连续变化的模拟信号在一定的时间间隔内进行离散取样,即数字信号采样。接着,对采样后的数字信号进行量化,将其转换为离散的数值。然后,通过编码将量化后的数值进行转换,以便DAC能够识别和处理。最后,DAC将这些数字信号转换为模拟信号输出。DAC是数字系统和模拟系统之间的桥梁,具有广泛的应用领域。在音频处理中,DAC被用来将数字音频信号转换为模拟音频信号,以驱动扬声器和耳机,其性能对音频质量有着决定性的影响。在通信系统中,DAC用于将数字信号转换为模拟信号,以实现信号调制和解调。此外,在仪器仪表领域,DAC也被广泛应用于各种测量和控制设备中。STM32中的DAC(视芯片而定例如F407ZGT6是2个DAC通道,有些芯片不支持DAC功能)支持12模式的数据输入,可以双通道同时转换。本期我们将介绍如何使用STM32F407和CubeMX利用HAL库实现DAC的输出。(本来是想使用C8T6的,结果突然想起来C8T6)没有DAC。CubeMX配置在DAC通道中开启DAC,在F407ZGT6中DAC1对应PA4,DAC2对应PA5。OutputBuffer这里设置DAC的输出缓存使能,Trigger是DAC是触发方式,这里我们选择不触发(手动写入)。如果选择触发的话是从缓存区写入数据。这里顺带加一路ADC采样,因为手上没有示波器,所以使用ADC来进行查看。这里使用DMA进行采样,具体可以参考公众号之前的关于DMA+ADC采样的内容。STM32的DMA采样+FFT时域分析(STM32F407)但是相比于之前的,需要更改一些设置,首先是DMA的设置这里设置单词请求,而不是循环模式防止数据跑飞掉。 MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_DAC_Init();
MX_USART1_UART_Init();
MX_TIM1_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); //用来触发adc采样
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)AD_Value, ADLenth);//开启ADC
HAL_DAC_Start(&hdac,DAC_CHANNEL_1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(!HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0))
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)AD_Value, ADLenth);//开启ADC
HAL_Delay(20);
while(!HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0));
HAL_Delay(1000);
for(int i = 0;i<ADLenth;i++)
{
printf("A:%d\r\n",AD_Value[i]);
}
}
}
/* USER CODE END 3 */
}
我们编写一段代码。按下按键的时候设置DAC的值,开启DMA传输,再加上ADC采样,之后输出结果。可以看到,我们按下按键之后,ADC的值会呈阶梯状上升。三角波发生器在DAC配置中打开波形发生器,触发方式选择定时器2触发,三角波最大振幅511。之后开启定时器2,由于ADC的采样率是1000HZ,因此根据奈奎斯特采样定律,信号的最大频率不能超过500HZ,因此我们使用100HZ的三角波。定时器设置好时间之后,设置触发事件。芯片手册中简单的介绍了一下如何计算三角波的频率。当触发信号发生后,内部计数器的值就会+1,所以频率可以用如下公式计算:定时器频率/分频系数+1/Period/三角波最大值/2 HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); //用来触发adc采样
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); //用来触发DAC输出
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)AD_Value, ADLenth);//开启ADC
HAL_DAC_Start(&hdac,DAC_CHANNEL_1);
HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_L,0);//设置直流信号
正弦波发生器 首先是定时器触发(方便控制频率)。但是关闭波形发生器。添加DMA,模式选择循环模式。还有改变一下定时器的频率!其他几乎不做改变。 #define POINTS 256
#define MIN_VALUE 50
#define MAX_VALUE 650
#define SCALE ((MAX_VALUE - MIN_VALUE) / 2.0)
#defineOFFSET50#define M_PI 3.14159265
uint16_t SinWaveInt[POINTS];
int SinWave[POINTS];
void SinInit(void)
{
for (int i = 0; i < POINTS; i++)
{
double x = ((double)i / (POINTS - 1)) * 2 * M_PI;
double sin_value = sin(x); // 计算正弦值
SinWave[i] = (int)((sin_value + 1) * SCALE + OFFSET);
SinWaveInt[i] = (uint16_t)SinWave[i];
}
}
计算一个正弦表。这里的Points决定了分辨率,结合定时器触发频率决定了正弦波的信号。测试一下正弦表,输出的是正弦信号。我们之后将正弦信号表导入DMA中。 SinInit();
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)SinWave, POINTS, DAC_ALIGN_12B_R);
正弦表初始化,之后启动DMA传输,导入正弦信号。 测试正弦信号成功。 FM调制 #define POINTS 1024
#define MIN_VALUE 50
#define MAX_VALUE 650
#define SCALE ((MAX_VALUE - MIN_VALUE) / 2.0)
#defineOFFSET50#define M_PI 3.14159265
uint16_t SinWaveInt[POINTS];
int SinWave[POINTS];
void SinInit(void)
{
for (int i = 0; i [removed]
详细解析STM32中GPIO有四种模式输出模式
顾名思义是单片机对外输出信号,这里的信号也是高低电平
好多朋友开始学习单片机的时候都是从51单片机开始,也许大家的第一个程序都是点亮一个LED灯。这是大家第一次接触到GPIO,而51单片机的IO口是准双向口,同时具备输入输出的功能,使用上也是非常的简单。但是一到STM32,就会被GPIO的各种模式整的一脸懵逼,本期我们就介绍STM32的GPIO的各种模式。STM32的GPIO大致可以分为四类:输入模式、输出模式、模拟模式以及复用模式。而每个都有细致的区分和小类。输入模式首先是输入模式,顾名思义输入模式就是用来向单片机输入信号的,但是这里的信号需是符合STM32的高低电平信号。STM32单片机的IO口电平兼容CMOS电平和TTL电平。对于逻辑电平0,所代表的电压范围在0.8v以下,而大于2v的话则代表逻辑1。STM32工作电压范围为2V ≤ VDD ≤ 3.6V。对于COMS端口,逻辑0的电压范围为-0.3V ≤ VIL ≤ 1.164V。至于高电平,STM32支持5v和3.3v。具体须看STM32芯片手册以定。而输入模式根据上拉电阻的不同分为输入上拉、输入下拉、输入浮空,它决定着当没有信号输入的时候,GPIO的电平是高电平还是低电平亦或者是不确定电平。输出模式顾名思义是单片机对外输出信号,这里的信号也是高低电平,外部器件可以读取输出端口的高低电平以达到控制外部器件的目的。输出模式除了上拉下拉电阻用以避免浮空状态的高低电平之外,还区分了推挽输出和开漏输出。如图即为推挽输出的示意图,电流通过两个MOS管/三极管由控制引脚形成一推一挽,故名推挽。推挽输出的好处是其可以有较大的输出电流,如果说后续的负载需要较大的电流的话,推挽输出是一个非常好的选择。但是有利有弊,由于其推挽输出的电路结构,导致不能同时有两个推挽输出的IO口相连接。否则电流就会通过三极管/MOS管导通,导致GPIO烧毁,因此如果使用GPIO的推挽模式,则必须避免IO的相互连接,也就是线与。而另一种模式则是:开漏输出由于其上拉电阻的存在,因此开漏输出的IO可以实现线与。但是同样的是这个上拉电阻的存在,导致其输出能力会不如推挽输出,因此开漏输出通常用在总线应用上,例如I2C的SCL和SDA线通常会挂载多个设备,因此通常这时候我们通常就会使用开漏输出,尤其是使用模拟I2C通讯的时候千万要小心不要使用成推挽输出了。 模拟模式模拟模式通常应用在ADC或者DAC中。为了减少上拉电阻和下拉电阻对ADC采样或者DAC输出的影响,通常模拟输出会关闭上下拉电阻,保证模拟信号不失真。 所以如果你的ADC和DAC的准确度有问题的话,可以检查一下是否加了上下拉电阻而不是将IO配置为输入模式。 复用模式 复用模式通常作为外设使用,例如硬件I2C,硬件SPI,串口通信等等,复用下,通常这些IO会有其特定的功能,我们一般不将其作为普通的输入输出IO使用。最后还有一些特殊IO,例如时钟输入IO,事件检测IO等等。
STM32上小白也能快速部署机器学习模型工具——NanoEdgeAIStudio初使用体会
前几日,偶然看见ST官方推了一个视频,介绍其边缘计算模型训练软件NanoEdge AI Studio,并且还是免费使用。而我在嵌入式边缘计算部署中卡住的则是模型训练,获取和优化的步骤。因此这个软件如果可以快速训练部署的话实在是可以帮我的大忙。于是连夜赶忙去官网查阅其相关资料和下载软件。从官网(需要注册)下载和安装好NanoEdge AI Stuido之后,打开软件需要进行激活。在其官方文档中可以找到相关的离线激活教程,下载软件的时候需要填写申请邮箱,之后邮箱会收到一份激活邮件。PS:(这里千万不要使用离线激活,哎呀呀折腾死我了)将这串密钥填入NanoEdge中点击激活,会得到一串新的License key软件界面分为了以上五个主要部分:创建项目打开已有项目帮助文档开源数据集(这里不知道是不是我是离线的原因)工具栏测试NanoEdge这里我尝试这创建项目,用一个2分类模型(n-class中n=2),我们简单的来测试一下,简单的做一个信号识别吧,虽然很没有必要,但是主要起到演示效果。在项目配置中,除了传感器类型,基本没啥好讲述的。在传感器类型中,有许多选项给我们选择,主要是数据源的由来,Generic可以作为通用传感器输入,其轴数(Number of axes)由数据源决定,例如我们有一个三轴姿态传感器,那么其xyz轴就是数据就是三轴,我们先简单的设置一个一轴来使用。后面几个就不一一赘述了。选择芯片,这里支持大部分STM32系列。我们选择自己常用的芯片,这边我选择F4的,这里我居然看见了Arduino,Nano和Uno用户的福音了。在这里导入数据,文档说明支持TXT和CSV格式(不知道为什么机器学习的文件格式都避开xls)文档中具体说明了由三种导入方式,可能我们日常用的话会比较从串口导入吧(拜托看看DMA+ADC采样然后配合串口打印,真用上了,对ADC数据的模型构建,完了写这里越写越激动,感觉好多好多东西想去做,谁懂半夜两点钟写文章越写越清醒)用C语言简单的写一个正弦信号脚本。导入脚本,选择分割方式,选择预览数量。这里由于二分类问题,我们需要给每个类情况都进行导入(分类1,分类2)导入两路信号,一路正常信号,一路载波信号。 需要注意的是:首先信号数量要是2的倍数,第二信号上要有噪声,不能是太完美的信号,第三如果相邻的数据相同可能会被识别为过采样,因此需要设定好数据。全是勾勾就没有问题啦。OK,咱们开始跑模型,这里实际使用的情况应该需要多组数据。之后就是等待训练结束。训练结束后,其模型大小优化后Flash 4.3K(STM32F407ZGT6的FLASH有1M)完全存放的下这个模型。但是由于我的数据集问题,具体要看使用的训练集。之后编译我们的模型。保存我们训练好的模型。 之后几天出具体使用这个模型的示例!使用体会 总的使用下来来说,不需要什么过硬的专业技能,在数据除了最开始因为离线激活导致后面部署出了点问题之外几乎没有什么卡住我的地方(数据集的地方是没想到数据集太过于理想但是实际情况下肯定是没有我这么理想的)所以第一次使用大部分的时间都花在了官网的参考文献上了
嵌入式领域的巨大潜力发展方向!
其实这个话题我去年创建这个公众号之初就有文章想去写 边缘计算 ,尤其是机器学习方面。但是无奈这方面需要的技能点实在是太多了,包括神经网络的基本知识,机器学习,模型的建立以及如何部署到STM32等嵌入式芯片上,一整套的流程需要的基本功实在太深厚了。因此一直停滞不前。 边缘计算是一种分布式计算模型,其核心思想是将数据处理和计算资源放置在接近数据产生源头的边缘设备、传感器或用户设备上。通过这种方式,边缘计算能够提供更快速、实时的计算和数据分析能力,满足行业在实时业务、应用智能、安全与隐私保护等方面的基本需求。 通俗点来说,我们将原本需要上位机处理的大量运算的工作交给下位机处理,而机器学习就是一种需要大量的运算,需要占用极大的运算资源的一种数据处理途径。 这些年陆陆续续的也有在嵌入式方向的机器学习使用,例如AI小车,基于树莓派的手势识别,基于STM32的手势识别,人脸识别,手写数字识别等等。但是他们都有两个非常巨大的局限! 首先是学习周期很长,例如在STM32部署机器学习模型需要学习大量的前置知识,诸如我前面提到的模型训练,模型优化等,并且要学会将模型嵌入进STM32,虽然CubeMX这种有着能够快速嵌入机器学习模型的组件CubeAI,但是光模型的获取就已经卡死了百分之九十对其产生兴趣的使用者望而却步。 其次是由于大部分芯片的性能限制,因为首先模型的本身需要有一定的空间来存放,因此存储空间不够的芯片连模型文件本身也无法存放。并且由于运行模型通常是一个庞大的计算,因此对于芯片的运算能力有着极高的要求,主观的体现在主频、算法、硬件加速器上,而高性能的芯片通常意味着价格的极具飙升。 这可能也是嵌入式边缘计算无法迟迟的得到广泛应用的原因吧。 但是边缘计算的功能和应用前景却非常广大。 例如一张227*227*3(3是其RGB颜色)的图片,如果对其进行人脸识别,直接上传图片数据的话将会占用非常大的带宽。而如果我们可以部署边缘化的人脸识别,将人脸识别的结果(位置或者是否)传输上去,可能只需要一个字节或者几个字节即可。因此边缘计算的使用可以帮助我们更好的使用嵌入式芯片的性能。极大的节省了传输数据的带宽,为更多设备的部署提供了条件。 曾经有一个项目是识别负载网络的负载类型(例如RL并联,RL串联,LC并联等等)其本质上表现为扫频曲线的不同形状,可是我们在代码层面去识别这样子的网络类型就会异常的复杂。 但是如果从机器学习的角度来看,这个问题是一个很典型的分类问题,我们可以通过将收集到的扫频信号作为输入数据,用许多的数据来实现网络模型的建立,之后可以依据这个网络模型来让计算机自己识别出最后网络的类型,而最后的精度完全取决于模型的准确性。 而且随着这些年来嵌入式芯片的运算能力越来越强,很多芯片的主频都可以上GHZ的级别,完全有那个性能运行神经网络的模型。 并且随着编程环境的优化,开源性和移植性进一步的优化,例如STM32编程中CubeMX等软件的出现,随着时代的发展嵌入式神经网络的前景将越来越广阔。 所以接下来可能就会尝试去试试网络模型的构建来使用CubeMX的CubeAI实现一些STM32上的机器学习的项目,例如人脸识别呀~什么什么的。 之后几天会出一些关于其探索和注意事项。
一句话,让我的MAX30100血氧传感器直接宕机!
最近是毕设的高峰期,MAX30100血氧传感器也是一款典型的光电传感器被应用在心率测量,血氧测量方面。 前几期我们介绍了一个FreeRTOS实战项目应用于类似运动手环检测老人摔倒姿态以及血氧心率采集。 而当我部署其到ESP32时,其他都没有什么问题,唯独当MAX30100,MLX90614,MPU6050一起使用的时候出现了问题,不仅仅是MAX30100无法启动的问题,还有其他传感器都失效。 如图MPU6050和温度传感器都是正常使用的,唯独MAX30100无法输出数据。 后来仔细检查,发现是因为我在代码的主循环中加入了一个延时。 这里本意是想延时,降低其他传感器数据的发送频率,但是就会出现MAX30100无法工作的问题。 原因分析 原因是MAX30100的update函数,其函数的目的读取传感器的数据,而传感器的数据曲线是一个和心率有关的曲线。 之后再利用其库函数中的滤波和寻找心率算法,检测峰峰值的心率数据。 所以正常情况下我们应该启用一个定时器来实现数据的更新,例如15ms更新一次数据。 然而当我们在While循环中使用delay函数的时候,就会导致采样出现了延迟,本来一秒钟可以有将近1000个点,而加了延时之后,采样率就会骤降,所以就会导致MAX30100解析其峰峰值频率算法出现许多问题。 所以正确的解决办法就是将延时函数去掉!void loop()
{
WiFiClient client;
float ambientTemp = mlx.readAmbientTempC();
float objectTemp = mlx.readObjectTempC();
int16_t ax, ay, az;
mpu.getAcceleration(&ax, &ay, &az);
client.connect("192.168.4.2", 8081);
mpu.getAcceleration(&ax, &ay, &az);
String data = "Ambient Temp: " + String(ambientTemp) + " *C | Object Temp: " + String(objectTemp) +
" *C | Ax: " + String(ax) + " | Ay: " + String(ay) + " | Az: " + String(az);
pox.update();//更新数据
if (millis() - lastReportTime > REPORTING_PERIOD_MS) {
lastReportTime = millis();
data += " | Heart Rate: " + String(pox.getHeartRate()) + " bpm | SpO2: " + String(pox.getSpO2()) + "%" + "\r\n";
Serial.println(data);
client.write(data.c_str(), strlen(data.c_str()));//数据上传
}
}
将延时函数去掉之后,就可以正常的使用MAX30100和其他函数了。 确实,这次的经历给我们提供了一个宝贵的教训:在编写代码的过程中,我们不能过度依赖外部库。虽然这些库能够为我们提供便捷的功能和工具,帮助我们快速构建应用,但过度依赖它们却可能掩盖了潜在的问题。 当我们对库的底层实现方式缺乏了解时,一旦遇到错误或问题,我们就很难准确地定位问题所在。这可能导致我们花费大量的时间和精力去排查错误,甚至可能让我们陷入困境,无法找到有效的解决方案。 因此,我们应该在使用外部库的同时,也努力去了解它们的底层实现方式。这样,当遇到问题时,我们不仅能够更准确地定位问题,还能够更深入地理解问题的本质。同时,通过了解库的底层实现,我们还可以更好地优化我们的代码,提高应用的性能和稳定性。 当然,这并不意味着我们应该完全摒弃外部库的使用。相反,我们应该在合理使用外部库的同时,保持对底层实现的关注和学习。只有这样,我们才能在编写代码的过程中更加得心应手,避免不必要的麻烦和损失。
STM32的FreeRTOS实战项目(1):老人跌倒监测及心率检测报警装置
自之前出了一系列FreeRTOS的学习笔记系列还没有应用于实战。本期介绍使用FreeRTOS实现运动状态与心率状态的检测。 看展示视频可以直接拉到最后~ 源码可以加QQ群获取~~ 656210280 本来这期内容上周就会发出来了,但是因为我的代码开始是在F4上写的,然后移植到F1上面的时候却因为芯片的问题导致FreeRTOS一开启调度就会死机,所以导致耽搁了几天。 本期所用的材料为: STM32F103C8T6 MAX30100心率血氧检测模块 MPU6050姿态检测模块 0.96寸OLED显示模块 旨在制作一款:检测心率,检测运动姿态,当运动加速度太大的时候(加速度超过跌倒阈值的时候发送警报) 程序大纲 CubeMX配置时钟配置 由于是借来的核心板,因此高速时钟选择石英晶体振荡。 时钟源选择一个定时器,这里FreeRTOS不推荐使用系统滴答定时器,因为虽然系统滴答定时器的精度比较高,但是不方便RTOS进行任务调度,并且软件定时器可以根据实际需求进行裁剪和配置所以这里推荐使用一个定时器来替代系统滴答定时器。 外设配置 外设选择一个串口和两个硬件I2C,硬件I2C可以用于OLED,MAX30100以及MPU6050的通讯。 这里我将OLED挂在到I2C1上,MAX30100和MPU6050挂在到I2C2上,这里大家可以挂载到同一根I2C总线上,I2C总线上可以挂载多个设备。我使用两个是开始测试的时候OLED是单独测试的。 FreeRTOS设置 修改一下堆栈大小,防止等会分配任务的时候堆栈溢出。 创建一个任务作为MPU6050的数据采集,将这个任务的优先级设置为高优先级,因为检测跌倒的优先级应该是最高的。 此外,还有MAX30100的数据处理任务,MPU状态异常处理任务,OLED刷新任务。 这里由于我决定使用全局缓存区来存放和处理数据(而不是在任务内利用临时变量来处理任务) 设置一个定时器用来定时读取MAX30100代码,设置三个信号量分别用来用来通知MAX30100数据处理,MPU6050异常处理以及OLED刷新显示。 用一个事件组来标记各个状态,例如各模块的初始化状态,MPU6050异常状态。 KEIL代码 在Keil中FreeRTOS中,帮我们实现了初始化,但是我们需要自己开启定时器,并且设置定时时间为15ms。float x[3];//存放
int stepSum;
/* USER CODE END Header_MPURuning */
void MPURuning(void *argument)
{
/* USER CODE BEGIN MPURuning */
/* Infinite loop */
for(;;)
{
MPU6050_ReadAccel(&mpu6050, x, x+1, x+2);
//这里的x[3]是三个方向的加速度,按理说是要计算合加速度
//printf("B:%.2f\r\n",x[2]);
if(x[1]>1.5&&x[1]!=1.9)
{
stepNow = stepSum;
MpuErrorFlag = 1;
xSemaphoreGive(MPU6050EventHandle);
}
if(x[1]>0.7)
{
stepSum++;
}
osDelay(10);
}
/* USER CODE END MPURuning */
}
这里MPU6050不断获取加速度,通过调整阈值来判断是否跌倒。void MAXData(void *argument)
{
/* USER CODE BEGIN MAXData */
static int i = 0;
uint32_t DCRED;
update();
DCRED = -removeRedDcComponent(rawIRValue)*2;
Data[i++] = DCRED;
if(i==300)
{
xSemaphoreGive(MAXStartHandle);
i = 0;
}
/* USER CODE END MAXData */
}
MAX30100通过一个软件定时器,15ms采样率定时读取数据,将数据写入一个缓存区,当采集到300时,发送一个二进制信号量。void MAXUsing(void *argument)
{
/* USER CODE BEGIN MAXUsing */
/* Infinite loop */
for(;;)
{
if (xSemaphoreTake(MAXStartHandle,portMAX_DELAY)) {
xTimerStop(MAXDataHandleHandle,0);
smoothFilter(Data,smooth,BuffLenth,3);
int index;
float cot;
float sum;
float Heart;
// for(int i = 0;i<300;i++)
// {
// printf("A:%d\r\n",smooth[i]);
// }
for(int i = 0;i[removed]70&&smooth[i][removed]0)
{
i++;
if(i==BuffLenth-1)
{
break;
}
}
while(smooth[i][removed]23)//23对应心率173认为误差
{
sum+= index;
cot++;
//printf("A:%d\r\n",index);
}
}
}
Heart = (float)(sum/cot)*0.015;
Heart = 60/Heart;
HeartFre= Heart;
sum = 0;
cot = 0;
if((HeartFre>160&&HeartFre<250)||HeartFre<30)
{
xEventGroupSetBits(TaskStauteHandle,1<<0);//第0位置1
}
xSemaphoreGive(OLEDShowSemHandle);
xTimerStart(MAXDataHandleHandle,0);
}
}
/* USER CODE END MAXUsing */
}
MAX30100数据处理任务等待信号量的释放,当等待到信号量释放即关闭定时器,并且对数据进行处理。 在数据中提取中心率信息(还可以提取血氧信息,但是要校准,我没做校准,就不展示了) 假如统计到了异常心率,将事件组的第0位置1. 之后通知OLED函数刷新。void MPUERROR(void *argument)
{
/* USER CODE BEGIN MPUERROR */
/* Infinite loop */
for(;;)
{
int flag = 1;
if (xSemaphoreTake(MPU6050EventHandle, portMAX_DELAY) == pdPASS)
{
for(int i = 0;i[removed]2)
{
flag = 0;
}
osDelay(10);
}
if(flag)
{
xEventGroupSetBits(TaskStauteHandle,1<<1);//第1位置1
xSemaphoreGive(OLEDShowSemHandle);
}
}
/* USER CODE END MPUERROR */
}
}
在MPU6050数据异常处理任务中,如果两秒内(这里实际情况应该设置更长时间)没有步数增加,就标志异常位置,之后通知OLED刷新。void OLEDShow(void *argument)
{
/* USER CODE BEGIN OLEDShow */
char s[50];
/* Infinite loop */
for(;;)
{
if (xSemaphoreTake(OLEDShowSemHandle,portMAX_DELAY)) {
sprintf(s,"Heart:%.2f",HeartFre);
OLED_Clear();
OLED_ShowString(0,1,(unsigned char *)s,2);
EventBits_t Bits = xEventGroupGetBits(TaskStauteHandle);
if(Bits&1<<0)
{
//第0位异常,心率异常
sprintf(s,"Heart is Error");
OLED_ShowString(0,2,(unsigned char *)s,2);
}
if(Bits&1<[removed]
STM32的DMA采样+FFT时域分析(STM32F407)
在以前的内容中有分开介绍过STM32的ADC配合DMA采样以及STM32利用DSP库实现快速傅里叶变换,而当二者真正的结合到一起实现一个信号采样以及频域分析才可以发挥出很强大的功能。 本期我们就来演示,利用DMA+ADC+FFT实现信号采集与频域分析。CUBEMX配置 ADC配置这边除了之前的配置之外有一点区别,就是我们把循环采样给关掉,因为我们打算利用定时器来触发采样,这样子的话可以精准的控制采样率。 这里我们选用TIM1的输入捕获来触发,当TIM1输出PWM波的时候,会选取上升沿来触发ADC采样,将采样的数据存储到DMA中。 定时器中我们设置PWM的频率为500HZ,这代表着我们的采样率为500HZ,之后生成我们的代码。(这里非常奇怪,我明明觉得这么算好像是1000HZ但实际只有500HZ,我哪里算错了?)代码编写#define FFT_LENGTH 1024//采样长度
arm_cfft_radix4_instance_f32 scfft;//定义scfft结构体
float FFT_InputBuf[FFT_LENGTH*2]; //FFT输入数组
float FFT_OutputBuf[FFT_LENGTH]; //FFT输出数组
uint16_t AD_Value[1024] = {0};//存放ADC的值
上述代码需要定义在全局,否则可能会因为栈溢出(因为临时变量的定义是存放在栈中的)导致代码崩溃。 HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); //用来触发adc采样
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)AD_Value, FFT_LENGTH);//开启ADC
arm_cfft_radix4_init_f32(&scfft, FFT_LENGTH,0,1);
HAL_Delay(1000);//稍微等待一会ADC一轮转换结束
for(int i=0; i < FFT_LENGTH; i++)
{
FFT_InputBuf[2*i]=AD_Value[i]*3.3/4096; //实部
FFT_InputBuf[2*i+1]=0; //虚部
}
arm_cfft_radix4_f32(&scfft,FFT_InputBuf);
//arm_cmplx_mag_f32(FFT_InputBuf,FFT_OutputBuf,FFT_LENGTH); //取模得幅值
for(int i = 0;i<FFT_LENGTH;i++)
{
float32_t real = FFT_InputBuf[2 * i];
float32_t imag = FFT_InputBuf[2 * i + 1];
float32_t magnitude = sqrtf(real * real + imag * imag);
// 打印每个频率分量的模值
printf("Fre: %f \r\n", magnitude);
}
生成代码,我们由定时器触发采样,原始信号来自于一个250HZ的方波。 这里可以看到外面采集的方波。注意事项 (这里我的问题,根据奈奎斯特采样定理,采样频率应该至少是信号频率的两倍,否则会出现采样失真,因此为了能够顺利的采样,我将PWM的频率修改为100HZ) 这里也非常奇怪,我觉得PWM的频率应该是100HZ的,但是不知道为什么测出来是50HZ(难不成我又算错了?) 后来发现是我的分时系数算错啦~ 变换结果 然后来看看傅里叶变换之后的结果。 (这个软件是我自己写的串口示波器) 每个FFT点对应的频率(f)与采样频率(Fs)和FFT的点数(N)之间的关系可以用以下的数学公式表示: f = k × (Fs / N) 其中,k 是FFT结果的索引(从0到N-1),Fs是采样频率,N是FFT的点数(与采样点数相同)。 这里外设定的采样长度是1024,采样频率是500HZ,因此每个点之间的频率差是500/1024(这里按理来说1000更加方便计算但是不知道为什么1000就会卡死,1024就不会,这个也等我研究一下)。 然后用Vofa+来算一下频率(自己的串口示波器不能显示下标,真是失败,我这两天就加上去这个功能) 首先是开始的这个大信号,毫无疑问就是是直流信号(低频)所带来的幅度。 这里我给数据加一个直流滤波,就可以把这个很大的直流量给滤掉了。 for(int i = 0;i<FFT_LENGTH;i++)
{
sum+=AD_Value[i];
}
sum/=1024;
for(int i=0; i [removed]
GPIO模拟串口通信——完成串口中断接收
上期我们使用GPIO模拟串口成功的发送了数据,本期我们进一步实现串口接收的功能。 首先,因为波特率的统一,我们可以使用同一个定时器来接收和发送。 这里我们定义两个变量来设置发送和接收的标记。 其次,由于起始位是一个低电平,空闲状态的串口总线是高电平,因此当我们收到消息的时候,实际上是一个下降沿。 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(RXFlag==0)
{
RXFlag = 1;
Recive = 0;
}
}
因此我们将RX引脚设置为下降沿的外部中断,并且收到外部中断,并且如果RX空闲(RXFlag == 0)的话,就将接收缓存区(Recive)置零,之后将RX标志位置1,在定时器中可以处理。void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(TXFlag==1)
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_9,Data[num++]);
if(num == 10)
{
num = 0;
HAL_TIM_Base_Stop_IT(&htim1);//关闭定时器
TXFlag = 0;
}
}
/*
外部中断触发后接收信息
*/
if(RXFlag==1)
{
num++;
if(num>=1&&num<=8)
{
/*
从第二位开始总计八位,对应数据从低到高
*/
Recive = Recive|(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_10)<[removed]=1&&RXnum<=8)
{
/*
从第二位开始总计八位,对应数据从低到高
*/
Recive = Recive|(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_10)<<(RXnum-1));
}
RXnum++;
if(RXnum == 10)
{
RXnum = 0;
RXFlag = 0;
RXSTA = 1;
}
}
}
/* 中断回调函数 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(RXFlag==0)
{
RXFlag = 1;
Recive = 0;
RXSTA = 0;
}
}
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include [removed]
int Data[10];
int RXFlag = 0;
int TXFlag = 0;
int RXSTA = 0;
int TXSTA = 1;
unsigned char Recive = 0;
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
void MyPrintf(int s);
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
void MyPrintf(int s)
{
if(TXFlag==0 && TXSTA)
{
Data[0] = 0;
for(unsigned char i = 0;i[removed]> 1;
}
Data[9] = 1;
TXFlag = 1;
TXSTA = 0;
}
}
int fputc(int ch, FILE *f) {
// 发送单个字符
MyPrintf(ch);
// 返回发送的字符
return ch;
}
STM32串口重定向?利用GPIO翻转模拟串口!!并进行重定向!!
学习过STM32的朋友肯定对串口通信不陌生,在之后的学习中我们会利用GPIO翻转来模拟软件I2C和模拟软件SPI,但是为什么没有人利用GPIO来模拟串口通信呢? 本期我们将利用定时器来模拟串口通信。 首先我们要明白串口通信的帧格式,有一个低电平起始位,八个数据位(可选择校验位)+停止位组成。 需要注意的是,这里的数据位从低到高!!! 通常,我们不会设置校验位,选择一个停止位。 因此我们的一帧数据共一个字节+2位,总计十位。而波特率则是一秒钟可以传输多少位的数据。例如9600的波特率则代表着一秒钟传输9600位,总计960帧,所以总共960个字节,有效位960*8位代表数据。 以9600波特率为例,每一位的持续时间约为104us. 因此在CubeMX中我们可以设置定时器的触发时间为104us。int num = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_9,Data[num++]);
if(num == 10)
{
num = 0;
HAL_TIM_Base_Stop_IT(&htim1);//关闭定时器
}
}
我们在定时器中断回调函数中,将Data(一帧)中的数据逐位发送。void MyPrintf(int s)
{
Data[0] = 0;
for(unsigned char i = 0;i[removed]> 1;
}
Data[9] = 1;
HAL_TIM_Base_Start_IT(&htim1);
}
写一个函数用来写入串口数据,并且启动定时器。 MyPrintf('H');
HAL_Delay(100);
MyPrintf('e');
HAL_Delay(100);
MyPrintf('l');
HAL_Delay(100);
MyPrintf('l');
HAL_Delay(100);
MyPrintf('o');
HAL_Delay(100);
成功的模拟了串口!!! 属于我的串口重定向!int fputc(int ch, FILE *f) {
// 发送单个字符
MyPrintf(ch);
HAL_Delay(50);//稍微延时,等待发送完成
// 返回发送的字符
return ch;
}
一种基于三点式振荡电路的电感测量仪/ESP32无线LCR测量仪——硬件篇
在三大类基本器件——电阻、电容、电感中电感的测量无疑是最困难的。通常我们会利用电容和电感之间发生振荡,根据振荡的频率来计算电感值。 三点式振荡电路是非常常见的振荡电路。 如图,由C1/C2/L1为主要器件所组成的振荡电路即为三点式电容振荡电路。三点式电容振荡电路是一种基于电容元件和电感元件构成的振荡电路。其基本构造如下图所示,其中包括两个电容元件(C1和C2)以及一个电感元件(L1)。这种电路通常用于产生频率稳定的振荡信号,常见于射频电路和通信系统中。 我们将其稍作改动。 如图所示振荡电路,比起之前的振荡器具有更高的振荡幅度以及更加稳定的波形。 并且其电压范围会控制在正电压,并不会出现经典的三点式振荡电路的负电压问题。 可以看到,其谐振峰峰值高达12V,但是这个电压太高了,不过我们可以采样C1和C2之间,可以得到一个非常好的正弦波。 可以看到其峰峰值约为5V,这就是一个非常好的电压范围。 之后我们利用一个施密特触发器/阈值比较器将正弦波转换为方波。 我们可以用一个施密特触发器(74LS14)将正弦波转换为方波,之后可以利用单片机或者数字电路计数器将频率显示出来。 电路验证 我们利用面包板搭建电路进行验证,可以看到得到了一个稳定的正弦信号。不过由于该信号幅度不够大,我在后面利用三极管进行了放大。 放大后的波形也呈现周期性,并且其范围也在NE555的触发电平范围。 之后利用555构建成施密特触发器,将该波形转换为矩形波。 不过需要注意的是,色环电感并不能进行振荡,可能由于其品质因数Q值过低。 选用高品质因数的电感是进行振荡电路设计中的关键之一。高品质因数的电感具有以下优点:稳定性: 高品质因数的电感能够提供更稳定的振荡频率和振幅。品质因数(Q值)越高,电感的损耗越小,振荡电路的能量损失也就越小,因此振荡的稳定性更高。频率响应: 高品质因数的电感在整个工作频率范围内能够提供更均匀的响应。这意味着即使在变化的工作条件下,振荡电路的频率稳定性也会更好。抗干扰能力: 高品质因数的电感对外部干扰更不敏感。在实际应用中,振荡电路可能会受到来自其他电路或环境的干扰,而高品质因数的电感能够减小这种干扰对振荡电路性能的影响。 例如这种工字形电感或者高频电感线圈,减少因电感损耗而导致振荡电路不能工作。 利用这个原理,配合NE555测电阻和电容的方式,我们可以制作一个LCR测量仪,利用ESP32作为主控,可以将其做成无线LCR仪。 利用IC紧锁座来固定器件,使用按钮来确定器件类型,利用多路复用器来选择测量通道。之后利用ESP32的外部中断(频率计数器)来测量频率转换阻值/容值/感值。 选择利用TFTLCD(ST7785s)来显示数据,也可以制作一款上位机APP来接受ESP32的所传输的内容。
Arduino IDE : 3句话完成MLX90614红外测温传感器的数据并测量温度
MLX90614是一款数字红外温度传感器,由Melexis公司开发生产。它能够测量目标物体的表面温度而无需接触,通过测量红外辐射来实现温度测量,因此非常适用于需要避免与目标物体接触或者需要在高温环境中进行温度测量的应用。以下是MLX90614红外测温传感器的一些关键特性和功能:非接触式测温:MLX90614利用红外辐射技术,能够实现对目标物体表面的温度测量,无需与目标物体直接接触。数字输出:该传感器输出的是数字信号,可以直接连接到微控制器或数字设备进行数据处理和分析。高精度:MLX90614具有很高的温度测量精度,能够满足许多应用的要求。双温度测量:传感器内置了两个独立的温度测量单元,一个用于测量目标物体的表面温度,另一个用于测量传感器芯片的温度。广泛的工作温度范围:MLX90614适用于广泛的工作温度范围,包括常温环境和高温环境。低功耗:传感器具有低功耗特性,在使用过程中能够有效节省能量。I2C总线接口:MLX90614采用标准的I2C总线接口,方便与各种微控制器和其他数字设备进行通信和集成。小型化设计:传感器尺寸小巧,易于集成到各种设备和系统中。 本期我们将利用Arduino IDE 利用ESP32快速实现该温度传感器的初始化与数据获取。 Arduino IDE(集成开发环境)是一款用于编写和上传代码到Arduino板子上的软件工具。它是由Arduino开发团队开发的免费开源软件,旨在简化使用者对Arduino平台的开发和编程过程。 这也是我非常喜欢使用Arduino IDE的原因。 在Arduino IDE中安装MLX90614的库。 可以在其源文件中看见初始化的内容,默认使用的是Wire(D21,D22)所以我们将传感器的SCL连接到D21,SDA连接到D22。#include [removed]//包含I2C库
#include [removed]//包含MLX库
Adafruit_MLX90614 mlx;//定义一个mlx变量
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Wire.begin();//默认初始化D21,D22
mlx.begin();//默认函数即可
}
void loop() {
// put your main code here, to run repeatedly:
double Obj = mlx.readObjectTempC();//读取物体温度
double Amb = mlx.readAmbientTempC();//读取环境温度
Serial.println("物体温度:"+String(Obj)+"\r\n"+"环境温度:"+String(Amb));
delay(1000);
}
我们只需要定义一个MLX90614的变量,调用begin初始化,最后读取表面温度和测量温度。 之后将我们测量的温度进行输出。 打开串口助手查看。 闲置温度 可以看到在不进行测量的时候表面温度和环境温度的区别并不是很大。 手掌温度 贴上手掌后温度约为36.49℃,差不多就是人体温度。 液体温度 对准液体,约为26.91摄氏度。 然后这边修改一下代码,弄上无线传输。 冰箱保鲜层 这是将传感器置于冰箱保鲜层所测得的数据。 冰箱冷底层 冰箱冷冻层数据
如何利用ADI滤波器设计向导快速设计滤波器(嘎嘎好用)
今日给我布置了一个二阶巴特沃夫低通滤波器设计的任务,要求使用的是Multisim的仿真。 通常上设计滤波器是一件复杂的事情,需要设计复杂的运算,我们常用查表法来计算滤波器的参数,但是总归是有门槛的。 滤波器的本质是利用电容电阻器件对于不同频率的信号(电容)所表现的阻抗不同,利用滤波器可以实现对不同频率的信号进行不同程度的衰减,从而实现“滤波” 本期我们介绍如何使用ADI公司的滤波器设计工具向导来实现快速的滤波器设计功能并进行仿真。 ADI(Analog Devices Inc.)是一家知名的模拟与数字信号处理技术公司,他们提供了一系列的滤波器设计工具,以帮助工程师们进行滤波器设计与仿真。其中,ADI提供了一款名为“滤波器设计工具向导(Filter Wizard)”的软件,可以帮助用户快速设计滤波器并进行仿真。 通过在搜索引擎中搜索“ADI滤波器设计向导”来找到相关的链接,然后点击ADI官网的链接以进入滤波器设计向导页面。通常情况下,ADI官网提供了丰富的资源和工具,包括滤波器设计向导,以帮助工程师进行滤波器设计与仿真。 选择滤波器类型,这里我们选择低通滤波器。 查看主要的参数例如滤波器的通带和截止频率以及调整通带增益。 调整好滤波器参数,设置好通带范围和通带增益,选择滤波器响应(滤波器类型) 点击元件选择,这里我们可以调整我们的元件参数。 点击我想选择,可以选择ADI对应的芯片以及相关的参数。 在 Multisim 仿真中,我们可以轻松地调整电路元件的数值,以便优化我们的滤波器设计,而无需考虑实际制作过程中的限制。因此,我们可以专注于调整电容的数值,以获得更好的滤波效果。 通过逐步增加或减少电容的数值,我们可以观察到滤波器在不同频率下的响应变化。通过这种方式,我们可以找到最佳的电容数值,以确保在整个频率范围内都能获得理想的信号抑制效果。 在仿真环境中,我们可以快速进行这样的参数调整,并立即观察到其对滤波器性能的影响,而无需受制于实际制作过程中可能涉及的物理限制。这样,我们可以更快地优化失真器设计,以满足我们的要求,而无需担心实际制作中可能出现的问题。 在 Multisim 仿真中,我们已经构建了电路图,并准备好进行信号的频率扫描。在 Simulate(仿真)选项中,我们将对交流信号进行频率扫描以评估滤波器的性能。 通过频率扫描,我们观察到在信号频率达到 1kHz 时,滤波器已经展现出非常良好的抑制效果。这意味着在这个频率以上,滤波器有效地减弱了信号的干扰或传输。值得注意的是,在低频率(例如 100Hz)处,并未观察到明显的信号抑制。这可能表明,在这个频率范围内,滤波器的性能还有改进的空间,需要进一步调整参数或优化设计来提高在低频范围内的抑制效果。 这个结果对于我们评估滤波器的整体性能至关重要。我们可以进一步分析扫频结果,对滤波器的工作频率范围、幅度响应和相位响应等进行更深入的了解。通过这些分析,我们可以不断优化滤波器设计,以满足特定应用的性能要求。
基于STM32的快速傅里叶变换(FFT)
快速傅里叶变换(FFT)是一种数字信号处理中常用的技术,用于将 快 速 序列转换为频域表示。在嵌入式系统中,如基于STM32的微控制器,实现FFT可以帮助解决信号处理的需求,例如声音处理、图像处理等。本文将介绍基于STM32的离散傅里叶变换的原理、实现方法和应用。 FFT是一种将时域序列转换为频域表示的技术,它将一个序列的N个采样点映射到频域中N个频率分量。其数学表达式如下: 其中,x(n) 是输入序列,X(k) 是输出的频域表示。准备工作: Keil中的DSP库(Digital Signal Processing Library,数字信号处理库)是针对ARM Cortex-M处理器系列的一组软件库,用于提供各种数字信号处理功能的支持。这些库提供了一系列优化过的算法,可以帮助开发人员在嵌入式系统中高效地实现音频处理、图像处理、通信系统等各种信号处理应用。 因此我们需要在Keil中安装我们的DSP库。#include "arm_math.h" // 包含DSP库
首先包含我们的DSP库。#define FFT_LENGTH 100
// 输入序列
float32_t inputSignal[FFT_LENGTH*2];
// 输出序列,存储变换后的结果
float32_t outputSignal[FFT_LENGTH];
定义FFT的的输入和输出数组还有数组长度 arm_status status;
arm_cfft_radix4_instance_f32 fft_inst;
status = arm_cfft_radix4_init_f32(&fft_inst, FFT_LENGTH,0,1);
void arm_cfft_radix4_init_f32(
arm_cfft_radix4_instance_f32 * S,
uint16_t fftLen,
uint8_t ifftFlag,
uint8_t bitReverseFlag
);
定义一个状态变量用来显示FFT的初始化是否成功。 定义一个FFT的配置变量。 初始化FFT。 S:指向 arm_cfft_radix4_instance_f32 结构体的指针,该结构体定义了 FFT 实例的状态信息。 fftLen:FFT 的长度。 ifftFlag:指定是否进行逆变换。如果为 1,则表示初始化的是逆变换的 FFT;如果为 0,则表示初始化的是正变换的 FFT。 bitReverseFlag:指定是否进行比特翻转。如果为 1,则表示进行比特翻转;如果为 0,则表示不进行比特翻转。 在FFT算法中,比特(bit)反转是一种关键的步骤,用于将输入数据重新排列为正确的顺序,以便在后续的计算中进行有效处理。当进行快速傅立叶变换时,算法要求输入数据的顺序是按照特定的方式排列的。特别是在使用基于分治法的算法(如Cooley-Tukey算法)时,输入数据的顺序必须满足按照一定规律的排列。在实际的FFT实现中,最常见的方式是通过比特反转来重新排列输入数据。比特反转就是将输入数据的比特位(二进制位)的顺序进行颠倒。这是因为在FFT算法中,数据会被分组,并按照一定规则进行反转,以便在每个阶段的运算中,数据可以正确地与其它组合进行配对。举个简单的例子,假设有一个长度为8的数据序列,按照0到7的顺序排列:0 1 2 3 4 5 6 7在进行FFT时,需要按照一定规则重新排列这些数据。比特反转操作将会对这个数据序列进行如下的重新排列:0 4 2 6 1 5 3 7在FFT算法的每个阶段中,这种重新排列都会使得数据正确地与其它组合进行配对,从而实现快速傅立叶变换的计算。 进行FFT并转换为模值 arm_cfft_radix4_f32(&fft_inst,inputSignal); //FFT计算
arm_cmplx_mag_f32(inputSignal,outputSignal,FFT_LENGTH); //取模得幅值
对输入数组进行FFT变换,并将FFT的结果转化为模值。 测试 我们进行一个简单的测试 #defineFFT_SIZE1024#define SAMPLE_RATE 1000
#defineNUM_SAMPLES1000#define FREQ_OF_INTEREST 100
for (int i = 0; i < NUM_SAMPLES; i++) {
float32_t t = (float32_t)i / SAMPLE_RATE;
float32_t sin_value = sinf(2 * PI * FREQ_OF_INTEREST * t); // 计算正弦波值
inputSignal[i * 2] = sin_value; // 实部
inputSignal[i * 2 + 1] = 0; // 虚部
}
一千个点的采样值,频率假设为100HZ作为输入信号。 for (int i = 0; i < FFT_SIZE; i++) {
// 计算复数的模值
float32_t real = inputSignal[2 * i];
float32_t imag = inputSignal[2 * i + 1];
float32_t magnitude = sqrtf(real * real + imag * imag);
// 打印每个频率分量的模值
printf("Magnitude: %f\n", magnitude);
}
进行傅里叶变换后打印模值。 可以看到傅里叶变换执行成功。 for (int i = 0; i [removed]
C语言:i++ or ++i ? 先用再加和先加再用!递增递减运算符在函数调用中出现的大坑
C语言中有许多奇怪的运算符,本期我们介绍一下C语言中的递增运算符以及递减运算符。i++ 和 ++i 都等价于 i = i + 1;
他的目的是简化i = i +1;运算,但是++i和i++表现在行为上的不同,++i是前置递增运算符,它先将 i 的值增加1,然后返回增加后的值。换句话说,它会先执行递增操作,然后再进行其他操作。 可以看到,当我们赋值的时候,i++的时候此时i先是5赋值给a,再递增,所以输出的时候,a是5,i是6。 而当我们使用++i的时候,i先进行递增,再将值赋值给a,这时候i的值为6,所以当我们输出a时候,结果是6。 在代码中常用这种方式,我们可以简化我们的代码。 for (int i = 0; i [removed]
为了C++尝试放弃Keil5转而使用STM32CubeIDE
从学习51单片机开始我的编程软件一直是Keil5,无论是C51还是MDK总是基于Keil的编程,除了使用TI公司的CCS编程过MSP系列,几乎没有使用过其他的编程IDE(除了编程ESP32的Arduino IDE)但是相比于Arduino IDE,Keil5实在是太糟糕了,首先是不支持C++面向对象的编程(这里可能是我不知道怎么使用,但是使用C++就会异常报错),这导致很多时候开发效率大大的降低,例如前两日我移植美信公司的MAX30100代码就需要将官方例程的C++修改为C语言而我在Arduino IDE中使用ESP32开发MAX30100的代码非常简便,直接调用MAX30100的构造函数即可。而在Keil5中则需要耗费大量的时间将C++的代码修改为C语言的代码。此外,由于Keil的变量定义的问题不同文件之间的数据调用时常会出现问题,也非常影响开发。而且最近在往上部署FreeRTOS的时候也发现,CubeMX生成的IDE无法在最新版的Keil上兼容,需要对Keil的版本进行下降。所以我时常在想什么时候就放弃使用Keil5来编程了,尝试新的IDE。其实好多人没在使用Keil进行编程,而是选择了VsCode来编程STM32,而且VsCode也支持C++的编程,对代码面向对象编程的支持度非常的高。但是我在尝试一个新的选择:STMCubeIDE。STM32CubeIDE是我在CubeMX生成代码的时候看到的选项。所以我认为既然是ST公司官方推出的IDE,那么无论是对于STM32的兼容还是对CubeMX的兼容想必会做的非常优秀。所以我就去ST的官方下载了STM32CubeIDE去试试水。安装和启动STM32CubeIDE的过程非常简单并且启动速度也非常的快。而且按照网上的教程STM32CubeIDE可以安装中文拓展包。不过下载的时间非常非常慢(也可能是第一次启动的原因吧)我们从一个现有的CubeMX文件中新建工程。主要语言选择C++将main.c更改为main.cpp这样子我们就可以使用C++语言的内容啦。 双击CubeMX文件,我们可以直接更改文件,然后修改初始化代码,可以快速更改代码。
/* USER CODE BEGIN Includes */
class Test
{
public:
void LED0Tog()
{
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
}
void LED1Tog()
{
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
}
};
/* USER CODE END Includes *
嵌入式电路基础(2)——按键电路详解/上拉电阻说明/按键消抖
上期介绍了“地”的实际意义,我们讲述了“地”本身只是一种参考,用来确认电路中参考0电势的存在。本期我们介绍一个嵌入式开发中常见的电路结构:按键电路按键电路的目的:我们能通过按键,使得按键电路能够输出高电平和低电平的一种电路。按键输入,电压输出。首先我们探讨一下这种电路,左边的是常见的按键电路。右边的是某抽象按键电路。毫无疑问的是,按键按下的时候,这个电路的输出点都可以输出低电平。但是右边的电路,当按键松开时,此时电阻R2没有和我们的电路隔离开来(这里认为单片机内部是很理想的虚无!)R2找不到一个任何可以参考的电位,在他的回路上!!!所以这时候你知道输出电压是多少嘛?单片机:啊,你以为我知道啊???这就构成了一个很忌讳出现的错误:悬空电路除了高电平和低电平,还出现了一个未知电平!为了避免这种情况的出现,我们在按键电路中引入一个电阻,如右图所示,当开关断开的时候,这个电阻从电压源中引入了一个确定的高电平作为按键电路的输出!由于我们的电路绘图习惯将电源置于上方,地置于下方,所以习惯的称这个电阻为:上拉电阻那么既然有上拉电阻,同样的还会有下拉电阻。诸位自行体会。但是通常我们的单片机,尤其是STM32这样子集成度比较高的单片机,可以配置内部上下拉电阻和输出模式(这里会单独出一期),因此我们的可以只需要一个开关?真的嘛?事实上,光有开关和上下拉电阻还不是一个完善的按键电路。通常我们的电路上需要加一个滤波电容!! C1的大小通常取常用的104电容/105电容。 由于按键的机械结构以及人体的不知名鬼畜抖动,我们按键按下的时候会发生许多抖动,这些抖动有可能会被单片机误认为高电平,因此极有可能造成单片机的误判。 而这些抖动本质上是高频信号,可以利用电容隔直通交的性质将高频信号导通到地(电容具有平滑波形的作用)(事实上是RC构成了低通滤波器对高频信号起到了抑制的作用) 当然我们也可以通过软件上面检测到按键之后等待一段时间按等待按键稳定的方式实现软件延时的消抖。 所以其实无论是软件上的延时,还是硬件上的延时都告诉我们,实际设计电路的时候需要考虑很多特殊情况,不能将电路太过于理想化。否则就会出现许多问题。而这些问题的解决方法和造成原因就是我们在不断试错的过程中经验积累所得。
STM32中CubeMX的FreeRTOS快速配置以及大量报错原因
之前出过一系列FreeRTOS的公众号,但是FreeRTOS的配置实在是啰嗦麻烦。需要耗费很长很长的时间。本期利用CubeMX中使用FreeRTOS快速的初始化。说明:由于CubeMX不支持MDK5.32以上版本,所以如果你是从官网下载的最新MDK的话。这时候编译版本只能使用Version 6,Version 6的编译速度比5快速很多,但是会导致RTOS的编译出现非常多的错误。这时候我们需要获取老版本MDK,并且装上Version 5,使用Version 5来进行编译,这样子虽然编译速度会慢很多,但是编译不会出错。因此如果大家编译出现了大量的错误的话,可以关注一下是不是因为MDK版本的问题。RTOS初始化首先将系统的时基切换成定时器(FreeRTOS推荐),这里推荐使用基本的定时器。在创建项目的左边点击展开Middleware and Software Pack...找到FREERTOS。选择CMSIS_V1为内核,在Include parameters中添加自己需要的头文件,这里我们启用二进制信号量。在Tasks and Queues中添加任务优先级修改为低优先级开启定时器,优先级设置为0(最高)在Timers and Semaphores添加二进制信号量和定时器。这里我们添加两个任务用来演示RTOS的并行逻辑。 可以看到,在文件的代码结构中多出来了RTOS的部分源码,这意味着我们可以使用RTOS,其实还有信号量我发现没有包括进去,也可以找到文件自行添加。void LED0Func(void *argument)
{
/* USER CODE BEGIN LED0Func */
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END LED0Func */
}
/* USER CODE BEGIN Header_LED1Func */
/**
* @brief Function implementing the LED1 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_LED1Func */
void LED1Func(void *argument)
{
/* USER CODE BEGIN LED1Func */
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END LED1Func */
}
在FreeRTOS.c文件中可以看到,系统帮我们定义好了我们的两个任务函数,不需要像RTOS裸机开发一样使用大量繁琐的配置和初始化。void LED0Func(void *argument)
{
/* USER CODE BEGIN LED0Func */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_10);//LED0反转
osDelay(500);
}
/* USER CODE END LED0Func */
}
/* USER CODE BEGIN Header_LED1Func */
/**
* @brief Function implementing the LED1 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_LED1Func */
void LED1Func(void *argument)
{
/* USER CODE BEGIN LED1Func */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_9);//LED1反转
osDelay(500);
}
/* USER CODE END LED1Func */
}
我们配置两个任务分别是控制两盏灯0.5s反转一次。 在main函数中不关调用了FREERTOS初始化也调用了任务调度器,因此我们只需要填充我们的任务函数。
嵌入式电路基础(1)——所谓的地究竟是什么?电流是流向地的嘛?
好吧,这个系列是很基础的电路系列,会从非常基础的电路开始讲起。主要是为了初学者服务,话不多说,直接开讲。本系列所用的仿真工具为Multisim。电源电源是电路中的最基础的部分,所谓的电流,也就是电荷的流动的宏观表现,而就像我们大学生一样,没有体测的压力是跑不动路的。驱动电荷流动的能量就是由电压提供的。电压的高低代表着驱动能力的大小,而限制驱动能力的器件,则是电阻。相同的电阻下,电压越大,电流越大。电压越小,电容越小。(只包含电阻的情况下)。生活中最常见的电源就是电池。在Multisim中可以用电压源来表示。那么在这个器件中A点电压和B点电压分别是多少?参考地事实上毫无意义,因为电压源只表示了电势差,即B和A点的电压之差为12V。为了计算的方便,我们在电路中引入了“地”的概念。其实我更愿意称“地”为参考地,因为“地”作为零电势点,确定了整个电路0点电压位置。它是一个概念,并不是一个实质的东西。只有给电路上添加了地,这时候才可以认为,A点电压是0V,B-A = 12V,所以 B点电压是12V。并不是因为电源是12V,所以B点是12V。因为,地只是一个参考,我可以把B点定为参考地。这时候,A点电压就是-12V,因为B-A = 12V , B = 0V,所以A = -12V。这就是为什么,我们在使用电源、示波器等设备,需要将设备的负极和电路接到一起。通常的说法是为电路提供回路,但是实际上是为设备提供0电压参考点。 电流方向既然地不存在,只是一个概念,那么怎么会有说:电流流向地呢?其实我觉得这是一种混淆的说法,对于我们外面的线路而言,大地是天然的导体,所以为了使电流能够构成回路,所以会将负极插入大地,这时候地是真正存在的,作为负极的存在也是0电势的存在。但是对于我们的电路而言,地实打实的是一种概念,事实上电流从流向地是不准确的。电流是从正极流向负极(初中课本原话,电子从负极流向正极,电流从正极流向负极)。这样子接的话,电流的方向是从正极流向负极,但是也会有一种从地流向负极的感觉。因此根据这个性质,可以把两个电流源分开成单电源和双电源。
STM32硬件IIC点亮一个OLED·显示心率血氧
之前介绍了STM32软件IIC和硬件IIC的区别,本期利用STM32上的硬件IIC实现OLED显示屏的使用。 这种0.96寸OLED可能是接触嵌入式的第一个IIC器件,大部分网上实现的方法都是利用软件IIC,但是软件IIC在不同设备之间的移植可能发生时序错误,本期我们利用硬件IIC来实现OLED屏幕的显示。 CubeMX中启用硬件I2C2,连接好SDA和SCL。 由于这块OLED的内部驱动是SSD1306,因此我们需要寻找SSD1306的驱动参考手册。uint8_t CMD_Data[]={
0xAE, 0x00, 0x10, 0x40, 0xB0, 0x81, 0xFF, 0xA1, 0xA6, 0xA8, 0x3F,
0xC8, 0xD3, 0x00, 0xD5, 0x80, 0xD8, 0x05, 0xD9, 0xF1, 0xDA, 0x12,
0xD8, 0x30, 0x8D, 0x14, 0xAF}; //初始化命令
void WriteCmd(void)
{
uint8_t i = 0;
for(i=0; i<27; i++)
{
HAL_I2C_Mem_Write(&hi2c2 ,0x78,0x00,I2C_MEMADD_SIZE_8BIT,CMD_Data+i,1,0x100);
}
}
//写命令
void OLED_WR_CMD(uint8_t cmd)
{
HAL_I2C_Mem_Write(&hi2c2 ,0x78,0x00,I2C_MEMADD_SIZE_8BIT,&cmd,1,0x100);
}
//向设备写数据
void OLED_WR_DATA(uint8_t data)
{
HAL_I2C_Mem_Write(&hi2c2 ,0x78,0x40,I2C_MEMADD_SIZE_8BIT,&data,1,0x100);
}
这个为基础,参考驱动手册编写清空的函数。void OLED_Clear(void)
{
uint8_t i, n;
// 遍历每一页(典型的SSD1306显示器有8页)
for (i = 0; i < 8; i++)
{
// 设置页地址
OLED_WR_CMD(0xb0 + i);
// 设置较低列地址为0
OLED_WR_CMD(0x00);
// 设置较高列地址为0x10
OLED_WR_CMD(0x10);
// 遍历每一列(典型的SSD1306显示器有128列)
for (n = 0; n [removed]> 4) | 0x10); // 设置较高列地址
OLED_WR_CMD(x & 0x0f); // 设置较低列地址
}
OLED_ShowNum 函数:参数:x、y 表示起点坐标,num 表示要显示的数字,len 表示数字的位数,size2 表示字体大小。作用:在OLED屏幕上显示一个数字。通过循环取出数字的每一位,根据字体大小在指定位置显示。可以选择填充模式或叠加模式。OLED_ShowChar 函数:参数:x、y 表示起点坐标,chr 表示要显示的字符,Char_Size 表示选择的字体大小。作用:在OLED屏幕上显示一个字符。根据选择的字体大小,选择相应的字体数组,然后在指定位置显示字符。OLED_ShowString 函数:参数:x、y 表示起点坐标,*chr 表示要显示的字符串,Char_Size 表示选择的字体大小。作用:在OLED屏幕上显示一个字符串。循环遍历字符串中的每个字符,调用 OLED_ShowChar 函数逐个显示字符。在显示每个字符后,横坐标 x 增加 8(字符宽度),当横坐标 x 超过一定范围后,纵坐标 y 增加 2(字符高度),横坐标 x 归零。//显示2个数字
//x,y :起点坐标
//len :数字的位数
//size:字体大小
//mode:模式 0,填充模式;1,叠加模式
//num:数值(0~4294967295);
void OLED_ShowNum(uint8_t x,uint8_t y,unsigned int num,uint8_t len,uint8_t size2)
{
uint8_t t,temp;
uint8_t enshow=0;
for(t=0;t<len;t++)
{
temp=(num/oled_pow(10,len-t-1))%10;
if(enshow==0&&t[removed]128-1){x=0;y=y+2;}
if(Char_Size ==16)
{
OLED_Set_Pos(x,y);
for(i=0;i<8;i++)
OLED_WR_DATA(F8X16[c*16+i]);
OLED_Set_Pos(x,y+1);
for(i=0;i<8;i++)
OLED_WR_DATA(F8X16[c*16+i+8]);
}
else {
OLED_Set_Pos(x,y);
for(i=0;i[removed]120){x=0;y+=2;}
j++;
}
}
这里需要有字库文件用来显示字符位置。 接着我们测试一下代码 OLED_Init();//OLED初始化
OLED_Clear();//刷新屏幕
OLED_ShowString(0,2,"Hello",2);//显示Hello
STM32中使用MAX30100/30102血氧测量仪算法简述
好久之前有一期关于使用ESP32利用MAX30100/30102制作一个血氧测试仪。 这里声明一下,我这个血氧模块的INT引脚并没有接,所以没办法使用INT引脚来触发采集。如果哪位朋友可以提供一下更好,简便的算法欢迎指正。 但是ESP32中使用MAX30100比较简单,利用官方美信的库(C++)就可以非常简单的使用血氧模块。 但是,当我们使用STM32来驱动MAX30100时,尤其是使用Keil这种不支持C++的IDE时,我们就会陷入一个非常头疼的处境,去修改心率算法。 这里不赘述30100初始化的操作以及如何进行读取数据,我将通讯用的I2C修改为硬件I2C方便不同设备之间的移植。 之后利用rawIRValue和rawRedValue来存放每次的数据。 在定时器中设置15ms读取一次心率数据。 我们将读取的心率数据进行打印。 可以看到,红外光数据会有一个一个波峰(后来我取了负)并且值通常大于50。 但是,当我们第一次把手放上去的时候,这个值会趋向无穷大。而这个波峰的地方,就是我们的心跳的位置。 算法简述 峰值统计 我开始采取了一个算法,统计某个时间内波峰的数量。 for(int i = 0;i[removed]50&&Red[i][removed]0)
{
i++;
}
}
}
当我们的值在50~100之间,我们认为统计到了一次波峰,并且等待下一个波谷的出现(0位置) 之后等待下一个波峰的出现。之后利用统计的数字,例如我这里总共采集了5s的时间,将这个数*12就是一分钟的心率。 但是这有一个非常的问题,我的分辨率注定卡在了12,如果需要提高分辨率就需要提高采样长度。 所以我就舍弃了这个算法。 间距法 第二种算法我想到了使用间距法,利用前一个峰值的思路,统计一下两个峰值之间的间距(多少个采样点)。结合采样周期计算心率。float Heart;
int index = 0;
for(int i = 10;i[removed]40&&smooth[i][removed]0)//等待波谷
{
i++;
if(i==size-1)
{
break;
}
}
while(smooth[i]<40)//等待第二个波峰
{
i++;
if(i==size-1)
{
break;
}
}
if(i<80)
{
index = i-index;
Heart = (float)(index)*0.015;
Heart = 60/Heart;
//printf("A:%d\r\n",index);
if(Heart[removed]40)
{
printf("A:%f\r\n",Heart);
}
}
}
}
我们统计两个间距之间的距离,并且结合采样率来计算心率。 这种方法的分辨率可以得到很好的提示,并且分辨率来自于采样率的缩短并且响应率可以比较高。 但是这种方法仍可能出现一些偶然值,所以我们可以进一步完善算法,例如利用去掉最大最小值的方法来平衡。 离散傅里叶变换 第三种方法也是我觉得最应该去使用的方法,既是使用离散傅里叶变换(DFT)将时域图转换成频谱图。 计算频率最大值是多少。 但是事实上我们的信号杂波很多,并且有很多干扰,试了一下离散傅里叶变换的效果好像不是很好。 这里我还没有做的好的效果,后续会单独出一期STM32利用DSP库进行离散傅里叶变换。
什么是IIC?STM32的硬件IIC和软件IIC的区别
IIC(Inter-Integrated Circuit)是一种常见的串行通信协议,广泛应用于各种嵌入式系统中,包括STM32单片机。在STM32中,IIC通信可以通过硬件IIC和软件IIC两种方式实现。本文将介绍IIC的基本原理,然后重点探讨STM32中硬件IIC和软件IIC的区别。 IIC基本原理: IIC是由飞利浦(Philips)公司提出的一种串行通信协议,适用于在同一电缆上连接多个设备。它采用两根线进行通信,即SDA(数据线)和SCL(时钟线)。设备之间通过这两根线实现数据传输,其中SDA用于传输数据,SCL用于同步时钟。 一对IIC总线上面可以挂载多个设备并且多个设备之间有不同的地址,所以我们可以根据不同设备的地址来实现不同设备之间的通信。 软件IIC 在STM32中,软件IIC是一种通过程序控制GPIO口模拟实现IIC通信的方法。这种实现方式常用于一些资源有限的应用场景,或者在需要更灵活控制IIC通信时使用。 总而言之,软件IIC是利用GPIO的翻转,一个IO模拟SCL线,一个IO模拟SDA线实现IIC通信协议的实现。 软件IIC不需要对IO有特殊的要求,只需要两个普通的GPIO即可实现,因此较为方便也方便移植,不同设备只需要重写IIC的基本通讯即可。// 定义IIC的GPIO口和引脚
#defineIIC_SCL_PINGPIO_PIN_6#define IIC_SCL_PORT GPIOB
#defineIIC_SDA_PINGPIO_PIN_9#define IIC_SDA_PORT GPIOB
// 定义读写控制位
#defineIIC_READ1#define IIC_WRITE 0
// 定义函数
void IIC_Start(void);
void IIC_Stop(void);
void IIC_SendByte(uint8_t byte);
uint8_t IIC_ReadByte(uint8_t ack);
// 启动IIC总线
void IIC_Start(void)
{
HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET);
HAL_Delay(2); // 稍微延时,确保时序正确
HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_RESET);
HAL_Delay(2);
HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_RESET);
}
// 结束IIC总线
void IIC_Stop(void)
{
HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_RESET);
HAL_Delay(2);
HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET);
HAL_Delay(2);
HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_SET);
HAL_Delay(2);
}
// 发送一个字节
void IIC_SendByte(uint8_t byte)
{
for (int8_t i = 7; i >= 0; i--)
{
HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, (byte & (1 <[removed]= 0; i--)
{
HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_RESET);
HAL_Delay(1); // 稍微延时,确保时序正确
HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET);
HAL_Delay(1);
byte |= (HAL_GPIO_ReadPin(IIC_SDA_PORT, IIC_SDA_PIN) <[removed]
ESP32 利用脉冲计数器+定时器计算测量方波频率(方波利用ESP32产生)
ESP32的功能非常强大(我现在已经是一名猛吹粉了)本期介绍ESP32使用其脉冲计数器来测量频率。 可以看到,ESP32支持最多八个通道的脉冲接收和发送。 因此本期将利用两块ESP32来进行实验。其中一块ESP32用来输出PWM波,并且利用电容触摸引脚实现触摸时递增输出频率。 另一块ESP32使用脉冲计数器统计PWM波频率,并且定时器设置1s进行统计得到频率数据。 配置频率计数器void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
attachInterrupt(digitalPinToInterrupt(15), counterISR, RISING);
}
unsigned int num = 0;
void counterISR()
{
num++;
}
void loop() {
// put your main code here, to run repeatedly:
Serial.println("A:"+String(num));
delay(1000);
}
首先是digitalPinToInterrupt 是一个Arduino函数,其作用是将数字引脚(Digital Pin)映射到相应的中断号(Interrupt Number),这里我们为了将引脚15映射到脉冲计数器的中断上。 当接收到脉冲时,对应的回调函数为counterISR(),这个函数名自己可以定义,在这个中断回调函数中我们将计数加一。 最后的一个参数为触发模式,通常脉冲有上升沿触发和下降沿触发,我们选择的是上升沿触发的模式。 在主函数中,我们延时一秒打印我们统计的脉冲数(这里我的脉冲源是10KHZ) 可以看到每次输出增加的都是10000 定时器配置
hw_timer_t *timer = NULL;//定时器句柄
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
attachInterrupt(digitalPinToInterrupt(15), counterISR, RISING);
timer = timerBegin(0, 80, true); // 选择定时器编号、分频器,true表示计数器自动重载
timerAttachInterrupt(timer, &timerISR, true); // 关联中断处理程序
timerAlarmWrite(timer, 1000000, true); // 设置定时器阈值为 1000000 微秒(1秒)
timerAlarmEnable(timer); // 启用定时器
}
unsigned int num = 0;
void counterISR()
{
num++;
}
int Fre;
void timerISR() {
// 定时器中断处理程序
Fre = num;
num=0;
}
void loop() {
// put your main code here, to run repeatedly:
Serial.println("A:"+String(Fre));
delay(1000);
}
我们设置定时器一秒钟触发一次,在定时器的中断回调函数中统计一下频率数实现精准的频率计数。 可以看到精准的10KHZ频率计数。 触摸改变PWM频率int Fre = 5000;
void setup() {
// put your setup code here, to run once:
ledcSetup(0, Fre, 8); // 通道 0,频率 5kHz,分辨率 8 位
ledcAttachPin(15, 0); // 将 PWM 通道 0 关联到 GPIO 15
ledcWrite(0,128);//通道0 设置占空比为50%
}
void loop() {
// put your main code here, to run repeatedly:
if(touchRead(4)<40)
{
while(touchRead(4)[removed]
C语言:认识逻辑运算符
对编程/嵌入式开发的朋友欢迎加入交流群:656210280在C语言中,逻辑运算符是程序员用来进行条件判断和逻辑运算的重要工具。逻辑运算符主要用于处理布尔值,即真(True)和假(False)。本文将介绍C语言中常用的逻辑运算符,以及它们在程序中的应用。1. 逻辑运算符的基础C语言中的三个基本逻辑运算符是AND(&&)、OR(||)和NOT(!)。这些运算符用于组合或改变条件表达式的真值。下面是它们的基本含义:AND运算符(&&): 当且仅当两个条件都为真时,整个表达式的值才为真。OR运算符(||): 只要两个条件中的任何一个为真,整个表达式的值就为真。NOT运算符(!): 用于取反,如果条件为真,则取反后为假;如果条件为假,则取反后为真。2. 逻辑运算符的使用示例让我们通过一些简单的示例来理解逻辑运算符在C语言中的应用。示例 1:AND运算符#include
int main() {
int age = 25;
int isStudent = 1;
if (age > 18 && isStudent == 0) {
printf("嘿 哥们,进去爽吧.\n");
} else {
printf("对不起,未满十八岁以及学生禁止进入\n");
}
return 0;
}
上述代码中,使用了AND运算符,只有当年龄大于18且不是学生时,条件才成立。示例 2:OR运算符#include
int main() {
int temperature = 28;
int isSummer = 1;
if (temperature > 30 || isSummer == 1) {
printf("太热了\n");
} else {
printf("浙江天气不是人待的\n");
}
return 0;
}
在这个例子中,OR运算符用于判断是否是炎热的天气或者是否是夏天(或者浙江)。示例 3:NOT运算符#include
int main() {
int isStudent= 0;
if (!isStudent) {
printf("进去吧!\n");
} else {
printf("学生禁止入内\n");
}
return 0;
}
在这个例子中,NOT运算符用于判断是否不是学生。3. 逻辑运算符的优先级在使用逻辑运算符时,需要注意它们的优先级。AND运算符的优先级高于OR运算符,因此在复杂的表达式中可能需要使用括号来明确优先级。并且通常更多的情况下我们会使用&&,||而不是&和|逻辑AND运算符 (&&): 当使用&&时,如果第一个条件为假,就不会再计算第二个条件了,因为整个表达式已经被确定为假。这种短路特性可以提高程序的效率。同样的||则是第一个条件为真,则不会去计算第二个条件。