全场景——(十一)综合实现
文章目录
- 一、产品框架
- 1.1 硬件框架
- 1.2 设计思路
- 1.2.1 上位机如何定位到传感器
- 1.2.2 上位机如何读写传感器
- 1.2.3 中控如何读写传感器
- 1.2.4 上位机如何发送映射关系、传输固件
- 1.2.5 上位机界面
- 1.3 软件框架
- 二、改造libmodbus 实现文件传输
- 2.1 分析Write File Record 功能
- 2.2 实现Write File Record
- 2.3 UART 驱动严重Bug
- 2.4 实现文件传输
- 三、读写任意传感器
- 3.1 上机演示
- 3.1.1 接线
- 3.1.2 烧写程序
- 3.1.3 使用
- 3.2 使用“点”的映射表操作任意传感器的原理
- 3.2.1 怎么表示映射关系
- 3.2.2 怎么发送映射关系
- 3.2.3 中控如何处理映射关系
- 3.3 中控代码详解
- 3.3.1 LibmodbusServerTask 任务
- 3.3.2 CH0_Task 任务
- 3.3.3 CH1_Task、CH2_Task 任务
- 3.3.4 loop_once 函数
- 四、IAP 升级
- 4.1 上机演示
- 4.1.1 接线
- 4.1.2 烧写H5 Bootloader 程序
- 4.1.3 烧写传感器Bootloader程序
- 4.1.4 编译H5 APP
- 4.1.5 编译传感器 APP
- 4.1.6 升级中控
- 4.1.7 升级传感器
- 4.2 IAP 升级的软件设计思路
- 4.2.1 Flash 划分
- 4.2.2 升级流程
- 4.2.3 点表:增加命令寄存器
- 4.2.4 Bootloader 软件设计
- 4.2.5 IAP 要点及时序图
- 4.3 中控Bootloader 代码详解
- 4.3.1 Bootloader 启动判断
- 4.3.2 处理启动命令
- 4.3.3 处理文件块(Write File Record)
- 4.4 传感器Bootloader 代码和中控APP代码详解
- 4.4.1 Bootloader 启动判断
- 4.4.2 处理启动命令
- 4.4.3 处理文件块(Write File Record)
- 4.4.4 中控APP要点
一、产品框架
1.1 硬件框架
1.2 设计思路
1.2.1 上位机如何定位到传感器
站在上位机的角度,以“Modbus传感器2为例”,上位机需要通过com1、channel1发出Modbus 数据包,数据包里含有“设备地址”、“寄存器类别”、“寄存器地址”。所以,上位机需要确定这几个参数:
① 串口:com1还是com2,或其他
② 通道:channel1还是channel2?可以使用channel0表示连接在主控上的设备
③ Modbus设备地址
④ Modbus设备的寄存器类别(DI、DO、AI、AO)
⑤ Modbus设备的寄存器地址
所以:在上位机的操作界面,增加一个“点”时,需要指定这5个参数。
1.2.2 上位机如何读写传感器
为了简化上位机的实现,在上位机的角度,它只看到一个传感器:就是主控
。上位机读写主控的寄存器时,主控会去读写其他传感器。这有一个“映射”:主控的寄存器,映射到channel0、1、2的其他设备的某个寄存器。当上位机增加“点”时,上位机会得到用户设置的“5个参数”,它会利用这些参数创建映射关系,并且发给中控。
1.2.3 中控如何读写传感器
中控程序得到上位机发来的“映射”信息后:
① 读操作:中控不断地读取传感器的某个点的数值,以备上位机读取。
② 写操作:中控等待上位机发来的写命令,根据这些写命令去写传感器。
1.2.4 上位机如何发送映射关系、传输固件
Modbus 里有“Write File Record”功能,可以使用它来发送大量数据、发送文件。
1.2.5 上位机界面
1.3 软件框架
二、改造libmodbus 实现文件传输
2.1 分析Write File Record 功能
想想这个场景,你要发出多个文件,每个文件还都很大,那么必然就是分块发送:
① 当前发送的是哪个文件?数据包里有“File Number”
② 当前发送的是第几个数据包?数据包里有“Record Number”
③ 当前发送的数据包多大?数据包里有“Record length”
④ 数据本身
“Write File Record”的请求格式如下:
从上往下解析表格数据:上图的Record还应该包含设备地址(1Byte)和CRC校验码(2Byte),去掉设备地址和校验码Record原来的256Byte就剩下256-1-2=253Byte,表格中第三行的len取决于下面数据总长度,所以len=7+2N,至于len的取值,继续减去功能码和数据总长度两个Byte为253-1-1=251=0xFB,此时数据大小为251-7=244Byte,为数据最大值,最小值计算如下:此时len=9,9-7=2,刚好对应上2NByte,所以数据最小值为2Byte。
“Write File Record”的回应跟请求包一模一样。
2.2 实现Write File Record
本 节 源 码为“E:\QuanCJ\h5_write_file_record”。
本节程序,在“E:\QuanCJ\Increase faulttolerance”d的基础上修改而来。
本节要实现2个功能:
① 改造libmodbus,增加“Write File Record”功能
② 测试:
a. H5 开发板的UART2连接到UART4
b. 任务1通过channel1(即UART2)发出“Write File Record”数据包
c. 任务2通过channel2(即UART4)得到“Write File Record”数据包,验证无误后再LCD 显示结果
按照下图连线:调试、供电、两个485互连:
主要代码如下:
int modbus_write_file_record(modbus_t *ctx,uint16_t file_no,uint16_t record_no,uint8_t *buffer,uint16_t len)
{int rc;int i;int byte_count;int req_length;int bit_check = 0;int pos = 0;uint8_t req[MAX_MESSAGE_LENGTH];/* 长度是2N */len = (len + 1) & ~0x1;/* ADU最大是256, 256-1字节设备地址-2字节CRC=253* 功能码等包头是7字节* 传输的数据最大253-9=244*/if (len < 2 || len > 244 )return -1;/* 包头 */req[0] = ctx->slave; //对应设备地址req[1] = MODBUS_FC_WRITE_FILE_RECORD; //对应功能码(0x15)req[2] = 7 + len; /* Request data length */ req[3] = 0x06; /* Sub-Req. x, Reference Type */req[4] = file_no >> 8; /* Sub-Req. x, File Number */ //req数组是uint8_t,file_no是uint16_t,将file_no数据存入req中需要分两次,同时注意大字节在前req[5] = file_no & 0x00ff;req[6] = record_no >> 8; /* Sub-Req. x, Record Number */req[7] = record_no & 0x00ff;req[8] = (len/2) >> 8; /* Sub-Req. x, Record length */ //这里需要除以2是因为len=2N,而一个N表示一个uint16_t的数据,除以2之后才能存入uint8_t的数组中req[9] = (len/2) & 0x00ff;req_length = 10;/* 数据 */for (i = 0; i < len; i++){req[req_length++] = buffer[i];}rc = send_msg(ctx, req, req_length);if (rc > 0) {/* Used by write_bit and write_register */uint8_t rsp[MAX_MESSAGE_LENGTH];rc = _modbus_receive_msg(ctx, rsp, MSG_CONFIRMATION);if (rc < 0)return -2;rc = check_confirmation(ctx, req, rsp, rc);}return rc;
}
代码分析如下:
从上面函数展开,对于Server,分为三个状态:
详细分成以下几步:
1、读取两个字节(设备地址一个字节,功能码一个字节),以便得到功能码:
2、读取完功能码(两字节),算出下一步要读取多少数据:
3、读取8个字节数据:
4、读取完8个字节,进入下一状态:
5、读取剩下的数据:
6、检查数据完整性:
添加相关调试代码:
app_freertos.c
static void CH2_UART4_ServerTask( void *pvParameters )
{uint8_t *query;modbus_t *ctx;int rc;modbus_mapping_t *mb_mapping;char buf[100];int cnt = 0;ctx = modbus_new_st_rtu("uart4", 115200, 'N', 8, 1);modbus_set_slave(ctx, 1);query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);mb_mapping = modbus_mapping_new_start_address(0,10,0,10,0,10,0,10);memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);memset(mb_mapping->tab_registers, 0x55, mb_mapping->nb_registers*2);rc = modbus_connect(ctx);if (rc == -1) {//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));modbus_free(ctx);vTaskDelete(NULL);;}for (;;) {do {rc = modbus_receive(ctx, query);/* Filtered queries return 0 */} while (rc == 0);/* The connection is not closed on errors which require on reply such asbad CRC in RTU. */if (rc < 0 ) {/* Quit */continue;}rc = modbus_reply(ctx, query, rc, mb_mapping);if (rc == -1) {//break;}cnt++;sprintf(buf, "server recv file record cnt = %d", cnt);Draw_String(0, 32, buf, 0xff0000, 0);if (mb_mapping->tab_bits[0])HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_RESET);elseHAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_SET);//vTaskDelay(1000);mb_mapping->tab_registers[1]++;}modbus_mapping_free(mb_mapping);vPortFree(query);/* For RTU */modbus_close(ctx);modbus_free(ctx);vTaskDelete(NULL);
}static void CH1_UART2_ClientTask( void *pvParameters )
{modbus_t *ctx;int rc;uint16_t val;int nb = 1;int level = 1;char buf[100];int cnt = 0;int err_cnt = 0;ctx = modbus_new_st_rtu("uart2", 115200, 'N', 8, 1);modbus_set_slave(ctx, 1);rc = modbus_connect(ctx);if (rc == -1) {//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));modbus_free(ctx);vTaskDelete(NULL);;}for (;;) {memset(buf, 0x5A, 100);rc = modbus_write_file_record(ctx, 1, 1, buf, 100);cnt++;if (rc < 0)err_cnt++;sprintf(buf, "client send file record cnt = %d, err_cnt = %d", cnt, err_cnt);Draw_String(0, 0, buf, 0xff0000, 0);/* delay 2s */vTaskDelay(2000);}/* For RTU */modbus_close(ctx);modbus_free(ctx);vTaskDelete(NULL);
}
上述代码中在Server中要注意打印调试代码的位置:当Server接收到数据后,应该先发送回应给Client才继续打印调试信息,不能先打印再发送给Client,不然Sever打印时太耗时导致Client等不到Server的回应就会出错,则err_cnt会为1,说明有一个错误。
现象如下:
Client和Server的cnt同步增加,没发生错误时err_cnt始终为0。
2.3 UART 驱动严重Bug
本节源码为“E:\QuanCJ\h5_uart_dma_ok”。
我们使用“HAL_UARTEx_ReceiveToIdle_DMA”来启动 UART 数据的接收,假设传入的数据长度是100。当数据接收完毕,或者发生了 Idle 中 断时 ,“HAL_UARTEx_RxEventCallback”函数被调用。但是!除了这两种情况之外,当接收到一半数据时,这个函数也会被调用。
在以前的代码里,我们误认为“数据全部接收完毕”、“发生Idle中断”时,才会调用“HAL_UARTEx_RxEventCallback”。在里面,我们认为数据接收完毕,于是再次启动DMA 传输。这会导致在接收到一半数据、触发“HAL_UARTEx_RxEventCallback”调用时,
我们也再次启动DMA传输,导致数据丢失。
正确的“HAL_UARTEx_RxEventCallback”函数代码如下:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{ PUART_Data pdata; static uint16_t old_pos; if (huart == &huart2) { pdata = &g_uart2_data; } if (huart == &huart4) { pdata = &g_uart4_data; } /* write queue : g_uart4_rx_buf Size bytes ==> queue */for (int i = old_pos; i < Size; i++) { xQueueSendFromISR(pdata->rxQueue, (const void *)&pdata->rx_buf[i], NULL); } old_pos = Size; if (huart->RxEventType != HAL_UART_RXEVENT_HT) { old_pos = 0; /* re-start DMA+IDLE rx */ HAL_UARTEx_ReceiveToIdle_DMA(pdata->huart, pdata->rx_buf, UART_RX_BUF_LEN); }
}
当使用libmodbus 时,有两个超时时间需要仔细考虑(Middlewares\Third_Party\libmodbus\modbus-private.h):
“_RESPONSE_TIMEOUT”是“等待回应的时间”
:client 给 server 发出请求后,client 要等待 server 发出的回应,这个等待时间要设置得比较大
,以便server有足够的时间处理数据并发出回应
。(这里的等待回应时间设置大一点可以让 Server 处理完并发送回应给 Client 同时也可以让 Client 接收后续的部分数据
)
“_BYTE_TIMEOUT”是“等待后续数据的时间”
:一旦得到回应的第一个数据后,后续的数据应该是连续不断地来到,所以后续的超时时间可以设置得比较小(比如设置为modbus 协议规定的3.5个字节时长)
。但是,对于DMA传输要特殊考虑
。
当使用DMA方式接收数据时,DMA可以及时地把串口接收到的数据存入内存,但是它无法及时通知应用程序
:假设启动DMA传输来读取256字节(modbus rtu最大数据包)数据,当读取到一半数据即128字节的数据时才会触发“Half Transfer”的回调,这时才会通知应用程序
。所以,使用 libmosbus 等待回应时,超时时间要设置得比较大,不仅要大于接收完 128 字节数据的时间
,也要考虑:留更多的时间给对方,以便它有足够的时间处理数据然后回复(这里是接收完前面的128个字节才去告诉应用程序 所以超时时间尽量设置大一点)
。
当应用程序读取到第一个回应的数据后,以后再读取后续数据时,超时时间如何设置?假设client 等待回应后得到了一半的数据共128字节,它需要等待多久才可以得到第129个字节数据?DMA很快得到第129个数据,但是要等待剩余数据全部接收完毕,这时才会发生中断,应用程序才被通知到
。得到回应之后(接收到一半的数据之后),DMA 传输完成还需要一半数据的时间
,以波特率115200为例,需要128x10/115200=11.1ms
。所以,使用libmosbus 传输比较大的回应数据时,xxx 超时时间设置为 10ms 是不够的,为了保险,我们可以设置为20ms。
修改后的代码如下:
2.4 实现文件传输
本节源码为“E:\QuanCJ\h5_write_file”。
对于“Write File Record”功能,它仅仅传输“一小块”数据,接收方无法从“这一小块”数据里知道“文件总大小”等信息。文件传输的功能需要我们自己实现:
① Client:使用文件名、文件大小,构造出Record Number为0的数据包,发送出去
这个数据包的格式为:
modbus.htypedef struct FileInfo { uint32_t version; uint32_t file_len; uint32_t load_addr; uint32_t crc32; uint8_t file_name[16];
}FileInfo, *PFileInfo;
② Server:收到这个数据包后,就可以从中知道文件名、文件大小等信息
③ Client:就可以把文件内容拆分为多个Record,发送出去
④ Server:接收到数据包后,根据Record Number组装数据
Client:
modbus.c/* 将小字节序转为大字节序 */
static uint32_t LE32toBE32(uint8_t *buf)
{return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | ((uint32_t)buf[3] << 0);
}int modbus_write_file(modbus_t *ctx,uint16_t file_no,uint8_t *file_name,uint8_t *buffer,uint16_t len)
{FileInfo tFileInfo;//该结构体包含要发送玫的文件信息int rc;//用来判断是否出错uint16_t record_no = 0;//Record的位置(从第0块开始发送)uint16_t pos = 0;//数据的位置uint16_t send_len = 0;//剩下数据大小memset(&tFileInfo, 0, sizeof(tFileInfo));tFileInfo.file_len = len;//长度赋给文件信息结构体的长度tFileInfo.file_len = LE32toBE32(&tFileInfo.file_len);//将小字节序转为大字节序if (file_name){strncpy(tFileInfo.file_name, file_name, sizeof(tFileInfo.file_name));}rc = modbus_write_file_record(ctx, file_no, record_no, &tFileInfo, sizeof(tFileInfo));//发送tFileInfo文件信息 主要包含该文件的文件名和总大小 以便后续可以将文件拆分成多个Record发出去if (rc < 0)//如果出错{return rc;}record_no++;//发送完一块Record,位置+1while (pos < len)//将数据还未发送完毕则一直循环发送{send_len = len - pos;//总数居减去当前数据位置等于剩余数据大小if (send_len > 240)send_len = 240; /* 选取一次发送240字节是为了便于烧录(烧录时一次烧写16字节) */rc = modbus_write_file_record(ctx, file_no, record_no, buffer + pos, send_len);if (rc < 0){return -5;}record_no++;pos += send_len;//每次发送完一个Record 就让pos加上当前Recorc剩余数据大小 让pos位置移到此处}return 1;
}
Server:
app_freertos.c/* 大字节序转换为小字节序 */
static uint32_t BE32toLE32(uint8_t *buf)
{return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | ((uint32_t)buf[3] << 0);
}static void modbus_parse_file_record(uint8_t *msg, uint16_t msg_len)
{uint16_t record_no;FileInfo tFileInfo;char buf[100];static int recv_len = 0;//当前接收到的数据长度if (msg[1] == MODBUS_FC_WRITE_FILE_RECORD)//根据上面数据包可知 接收的第一个数据是设备地址 第二个数据是功能码 这里判断功能码是否为0x15{record_no = ((uint16_t)msg[6]<<8) | msg[7];//根据上面数据包可知 第6、7个字节数据为Record number 将uint16_t数据放进uint32_T中需要字节序转换if (record_no == 0)//判断Record的位置是否为0 为0则说明可以得到后续数据的大小{tFileInfo = *((PFileInfo)&msg[10]);tFileInfo.file_len = BE32toLE32(&tFileInfo.file_len);sprintf(buf, "Get File Record for Head, file len = %d", tFileInfo.file_len);Draw_String(0, 32, buf, 0xff0000, 0);recv_len = 0;}else//Record的位置不等于0说明得到的是数据本身{recv_len += msg[2] - 7;//接收到一个数据后 需要加上其长度大小 显示当前所接收的数据长度sprintf(buf, "Get File Record %d for Data, record len = %d, recv_len = %d ", record_no, msg[2] - 7, recv_len);Draw_String(0, 64, buf, 0xff0000, 0); }}
}
}
现象如下:
发送文件数量一直增加,同时会获得文件的后续数据,我们在Client中设置大小为500,所以后续数据大小为500.
三、读写任意传感器
3.1 上机演示
3.1.1 接线
如下图连线,H5控制板使用USB线供电,中间的HUB也使用USB供电:
连接示意图为:
注意:三个传感器的启动开关都拨到“ON”位置。
3.1.2 烧写程序
先烧写中控(H5 开发板),工程为“E:\QuanCJ\h5_read_write_with_map”。
再分别烧写三个传感器,工程为“E:\QuanCJ\Increase fault tolerance\f030_demo”。
注意:f030_demo需要分别设置下面的宏(这3个宏同一时间只能定义一个),编译出程序后分别烧写到3个传感器里。
#define USE_SWITCH_SENSOR 1
#define USE_ENV_MONITOR_SENSOR 1
#define USE_TMP_HUMI_SENSOR 1
3.1.3 使用
先启动上位机:
再增加、设置“点”。
比如要操作接在CH2上的温湿度传感器的蜂鸣器1,如下设置:
比如要操作中控(H5开发板自带的LED),如下设置:
注意:
① 在上位机里进行设置时,无法使用实体键盘(LVGL 前台程序使用键盘有点难度),需要使用它弹出的虚拟键盘。
② 为了有空间显示虚拟键盘,当前要设置的“点”,会自动移到屏幕的最上面。
最后就可以读写点了,比如:
3.2 使用“点”的映射表操作任意传感器的原理
在前面文章中的上位机访问多个传感器曾经解析过“点”的映射,它使用固定的映射表。此处将使用更加灵活的、随时可变的映射表。
上位机要读写传感器,需要中控进行转发。在上位机的角度,它只看到中控、只是读写中控的寄存器。中控需要“帮助”上位机去读写下挂的传感器。这里存在“点”的映射关系:上位机要读写的主控的“点”,怎么跟具体传感器的“点”对应。
使用“点”的映射信息,有3个问题要解决:
3.2.1 怎么表示映射关系
使用如下结构体表示一个点的映射关系:
typedef struct PointMap { char reg_type[4]; /* 寄存器的类别(DI、DO、AI、AO) */uint16_t reg_addr_master; /* 主控的寄存器地址 */ uint16_t channel; /* 0-主控本身, 1-通过CH1访问, 2-通过CH2访问 */ uint16_t dev_addr; /* 传感器的设备地址 */ uint16_t reg_addr_salve; /* 传感器的寄存器地址 */
}PointMap, *PPointMap;
主控根据这样的结构体,就知道:上位机想读写“reg_type”类型的“reg_addr_master”寄存器时,实际上是想读写“channel通道上、设备地址为dev_addr的reg_type 类型的reg_addr_slave”寄存器。
上位机会把多个“PointMap”发给中控,中控要记录这些结构体。
3.2.2 怎么发送映射关系
使用modbus_write_file 发送 file_no为 0 的数据。
3.2.3 中控如何处理映射关系
中控创建4个任务:
①.USB 串口任务:用于实现USB串口
② LibmodbusServerTask:跟 PC 进行通信,接收上位机发来的“点”的映射表;根据上位机的读写请求操作DI/DO/AI/AO寄存器
③ CH0_Task:中控上有CH1、CH2,分别对应2个RS485接口。我们引入CH0,表示要读写中控板子自带的传感器(比如LED)。
CH0_Task 有 2 个功能:
a.根据映射表,读取中控本身的传感器,更新DI/DO/AI/AO寄存器
b.当映射表里channel 0对应的DO/AO寄存器发生变化时,用来设置中控本身的传感器
④ CH1_Task:它有2个功能
a.根据映射表,读取channel 1的传感器,更新DI/DO/AI/AO寄存器
b.当映射表里channel 1对应的DO/AO寄存器发生变化时,用来设置对应的传感器
⑤ CH2_Task:它有2个功能
a.根据映射表,读取channel 2的传感器,更新DI/DO/AI/AO寄存器
b. 当映射表里channel 2对应的DO/AO寄存器发生变化时,用来设置对应的传感器
3.3 中控代码详解
源码在“E:\QuanCJ\h5_read_write_with_map”中,解析的代码主要在“Core\Src\control.c”里。
3.3.1 LibmodbusServerTask 任务
它有2个功能:
① 跟PC进行通信,接收上位机发来的“点”的映射表;
② 根据上位机的读写请求操作DI/DO/AI/AO寄存器
代码如下:
/*********************************************************************** 函数名称: modbus_parse_file_record* 功能描述: 解析文件信息, 目前仅仅解析file_no为0的信息(点的映射表)* 输入参数: msg - 里面存有上位机发来的消息* msg_len - 消息长度* 输出参数: 无* 返 回 值: 无***********************************************************************/
static void modbus_parse_file_record(uint8_t *msg, uint16_t msg_len)
{uint16_t file_no;uint16_t record_no;if (msg[1] == MODBUS_FC_WRITE_FILE_RECORD){file_no = ((uint16_t)msg[4]<<8) | msg[5];record_no = ((uint16_t)msg[6]<<8) | msg[7];if (file_no == 0){/* 处理"点"的映射信息 */parse_map_info(msg, msg_len);//根据数据包 msg[10]对应后续的数据 该函数主要将msg[10]拷贝进一个映射表的结构体} }
}
3.3.2 CH0_Task 任务
它有2个功能
① 根据映射表,读取中控本身的传感器,更新DI/DO/AI/AO寄存器
② 当映射表里channel 0对应的DO/AO寄存器发生变化时,用来设置中控本身的传感器
代码如下:
/*********************************************************************** 函数名称: CH0_Task* 功能描述: 中控任务, 用来读写中控自己的传感器* 输入参数: pvParameters - 就是mb_mapping(里面含有DI/DO/AI/AO寄存器)* 输出参数: 无* 返 回 值: 无***********************************************************************/
static void CH0_Task( void *pvParameters )
{modbus_mapping_t * mb_mapping = pvParameters;while (1){loop_once(NULL, 0, mb_mapping);//在这里CH0处理的是主控自己的寄存器 不需要发起modbus传输来读取外接的传感器 所以参数为NULLvTaskDelay(100);}vTaskDelete(NULL);
}
3.3.3 CH1_Task、CH2_Task 任务
它们功能类似,都有2个功能
① 根据映射表,读取channel 1或channel 2的传感器,更新DI/DO/AI/AO寄存器
② 当映射表里channel 1或channel 2对应的DO/AO寄存器发生变化时,用来设置对应的传感器
代码如下(以CH1_Task为例):
/*********************************************************************** 函数名称: CH1_Task* 功能描述: CH1任务, 用来读写接在CH1上的传感器* 输入参数: pvParameters - 就是mb_mapping(里面含有DI/DO/AI/AO寄存器)* 输出参数: 无* 返 回 值: 无***********************************************************************/
static void CH1_Task( void *pvParameters )
{modbus_mapping_t * mb_mapping = pvParameters;modbus_t *ctx;int rc;ctx = modbus_new_st_rtu("uart2", 115200, 'N', 8, 1);rc = modbus_connect(ctx);if (rc == -1) {//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));modbus_free(ctx);vTaskDelete(NULL);;}while (1){loop_once(ctx, 1, mb_mapping);vTaskDelay(100);}vTaskDelete(NULL);
}
3.3.4 loop_once 函数
它是CH0_Task、CH1_Task、CH2_Task 的主要函数,代码如下:
/*********************************************************************** 函数名称: loop_once* 功能描述: CH0_Task/CH1_Task/CH2_Task任务的一次循环函数* 输入参数: ctx - modbus上下文* channel - 通道* mb_mapping - 里面含有DI/DO/AI/AO寄存器* 输出参数: 无* 返 回 值: 无***********************************************************************/
static void loop_once(modbus_t *ctx, int channel, modbus_mapping_t *mb_mapping)
{int count = get_point_map_count();//获得映射表中的点数int i;int val;int pre_val;/* 如果modbus_mapping_t里这个channel的点发生了变化,发起写操作 */for (i = 0; i < count; i++)//遍历所有的点{/* channel不等于0,表示要使用modbus函数访问外接的传感器 */if (g_tPointMaps[i].channel == channel && channel){get_modbus_mapping_reg(&g_tPointMaps[i], mb_mapping, &val);//从AI、AO、DI、DO寄存器取出值valif (val != g_iPointVals[i])//如果val不等于之前记录的值 则说明上位机修改了新值 需要进行写入对应的传感器{if (0 == modbus_write_point(ctx, &g_tPointMaps[i], val)){update_modbus_mapping_reg(&g_tPointMaps[i], mb_mapping, val);g_iPointVals[i] = val;//写完之后更新val的值}}}/* channel等于0,表示要访问中控自己的传感器 */if (g_tPointMaps[i].channel == channel && !channel){get_modbus_mapping_reg(&g_tPointMaps[i], mb_mapping, &val);if (val != g_iPointVals[i]){if (0 == local_write_point(&g_tPointMaps[i], val)){update_modbus_mapping_reg(&g_tPointMaps[i], mb_mapping, val);g_iPointVals[i] = val;}}}}/* 然后更新映射表里channel通道的所有点 */for (i = 0; i < count; i++){/* channel不等于0,表示要使用modbus函数访问外接的传感器 */if (g_tPointMaps[i].channel == channel && channel){if (0 == modbus_read_point(ctx, &g_tPointMaps[i], &val))update_modbus_mapping_reg(&g_tPointMaps[i], mb_mapping, val);}/* channel等于0,表示要访问中控自己的传感器 */if (g_tPointMaps[i].channel == channel && !channel){if (0 == local_read_point(&g_tPointMaps[i], &val))update_modbus_mapping_reg(&g_tPointMaps[i], mb_mapping, val);}}
}
四、IAP 升级
IAP 是In Application Programming 的首字母缩写,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。
4.1 上机演示
4.1.1 接线
如下图连线,H5控制板使用USB线供电,中间的HUB也使用USB供电:
连接示意图为:
注意:三个传感器的启动开关都拨到“ON”位置。
4.1.2 烧写H5 Bootloader 程序
先烧写中控(H5开发板),工程为“E:\QuanCJ\IAP upgrades\h5_iap”。
注意:H5的BootLoader和APP是同一套代码,需要指定不同的ROM地址,如下设置即为Bootloader。
4.1.3 烧写传感器Bootloader程序
分别烧写三个传感器的 Bootloader,工程为“E:\QuanCJ\IAP upgrades\f030_iap”。
注意:传感器的BootLoader和APP是同一套代码,需要指定不同的ROM地址。
先如下设置ROM地址:
注意:f030_iap需要分别设置下面的宏(这3个宏同一时间只能定义一个),编译出程序后分别烧写到3个传感器里。
再修改“f030_ipa\demo\Core\Src\freertos.c”,定义宏开关:
#define USE_SWITCH_SENSOR 1
#define USE_ENV_MONITOR_SENSOR 1
#define USE_TMP_HUMI_SENSOR 1
编译后,就可以分别烧录到对应的传感器。
4.1.4 编译H5 APP
可以使用如下目录有事先编译好的APP:
也可以自己编译。工程为“E:\QuanCJ\IAP upgrades\h5_iap”。
注意:H5的BootLoader和APP是同一套代码,需要指定不同的ROM地址,如下设置即为APP。
编译得到bin文件:demo_h5_app.bin。
最后,把bin文件放到上位机程序相同的目录下,这点很重要。由于LVGL的限制,目前只支持使用同目录下的文件。
4.1.5 编译传感器 APP
可以使用如下目录有事先编译好的APP:
也可以自己编译。工程为“E:\QuanCJ\IAP upgrades\f030_iap”。
注意:传感器的BootLoader和APP是同一套代码,需要指定不同的ROM地址。
先如下设置ROM地址:
注意:f030_iap需要分别设置下面的宏(这3个宏同一时间只能定义一个),编译出程序后分别烧写到3个传感器里。
再修改“f030_ipa\demo\Core\Src\freertos.c”,定义宏开关:
#define USE_SWITCH_SENSOR 1
#define USE_ENV_MONITOR_SENSOR 1
#define USE_TMP_HUMI_SENSOR 1
编译后,就可以得到“demo_f030_app.bin”。对于不同的传感器,可以把bin文件分别改名为:
demo_f030_app_ch1_dev1_switch.bin
demo_f030_app_ch1_dev2_env_monitor.bin
demo_f030_app_ch2_dev3_temp_humi.bin
最后,把bin文件放到上位机程序相同的目录下,这点很重要。由于LVGL的限制,目前只支持使用同目录下的文件。
4.1.6 升级中控
先启动上位机:
在界面上如下操作:
在上位机界面,可以看到升级进度:
在中控的 LCD 屏幕,在升级过程中显示“Bootloader”,升级完成后显示“Application”。
然后就可以按照前面的上机演示使用中控了。
4.1.7 升级传感器
先启动上位机:
在界面上如下操作:
在上位机界面,可以看到升级进度:
在中控的LCD屏幕,在升级过程中显示“Send file to sensor: record_no =”类似的打印信息。
升级完成后,可以看到传感器的三个LED 亮起。然后就可以按照前面的上机演示使用传感器了。
4.2 IAP 升级的软件设计思路
4.2.1 Flash 划分
对于中控,STM32H563RIV内置2MB Flash,划分如下:
① Bootloader 占据256KB空间
② APP占据1784KB空间
③ 配置信息占据最后一个扇区8KB空间:用来保存APP版本、大小、校验码等信息。
对于传感器,STM32F030CC内置256KB Flash,划分如下:
① Bootloader 占据128KB 空间
② APP占据126KB空间
③ 配置信息占据最后一个扇区2KB空间:用来保存APP版本、大小、校验码等信息。
4.2.2 升级流程
升级流程分为3个步骤:
(1) 上位机让目标板进入Bootloader
无论是中控还是传感器,它当前运行的程序可能是“Bootloader”或“APP”。要升级APP,必须让它进入Bootloader:
① 对于中控:上位机直接发命令给中控让它进入Bootloader
中控处于Bootloader的话则无需重启;中控处于APP的话,需要设置配置信息表明要
进入Bootloader,然后软件复位;重启后运行的是Bootloader,它要根据配置信息保持在Bootloader。
② 对于传感器:上位机直接发命令给中控,中控再转发给传感器让它进入Bootloader
传感器处于Bootloader的话则无需重启;传感器处于APP的话,需要设置配置信息表明要进入Bootloader,然后软件复位;重启后运行的是Bootloader,它要根据配置信息保持在Bootloader。
注意:升级传感器时,中控要运行APP
:它功能比Bootloader 强大,可以更方便地操作传感器。
所以:要升级中控,中控要运行 Bootloader。要升级传感器,中控要运行 APP,传感器要运行Bootloader
。
(2) 上位机发送固件、目标板接收到固件后烧录
(3) 上位机让目标板进入APP
当上位机发送完固件后,再给中控或传感器发送“启动 APP”的命令。目标板就要设置配置信息表明要进入 APP,然后软件复位;重启后运行的是 Bootloader,它要根据配置信息启动APP。
上位机程序流程图如下:
中控和传感器的Bootloader程序流程图如下:
4.2.3 点表:增加命令寄存器
无论是中控还是传感器,都需要接收“启动Bootloader”、“启动APP”的命令。可以增加一个点:地址为0,类型为“AO”(4x)。
① 写入0x55,表示“启动Bootloader”,即:让目标板进入Bootloader状态
② 写入0xAA,表示“启动APP”,即:让目标板进入APP状态
最新点表如下:
① 中控寄存器说明:
② 开关量模块(SWITCH)寄存器说明:
③ 环境监测模块(ENV_MONITOR)寄存器说明:
④ 温湿度模块(TEMP_HUMI)寄存器说明:
4.2.4 Bootloader 软件设计
Bootloader 升级 APP 的流程步骤看起来很多,其实它就做3件事:
① 启动:根据配置信息,决定保留在Bootloader还是启动APP。
② 处理启动命令:处理接收到“进入Bootloader”的命令、或“进入APP”的命令。
③ 处理文件块:中控的Bootloader 需要处理映射信息、固件信息。传感器只会接收到固件信息,只需要处理固件信息。
处理映射信息:对于接收到的“Write File Record”数据包,如果record_no为0则解析出文件大小;如果record_no不为0则用来记录映射表。
处理固件信息:对于接收到的“Write File Record”数据包,如果record_no为0则解析出文件大小;如果record_no不为0则用来烧录Flash。
4.2.5 IAP 要点及时序图
要点有2个:
① 在升级中控APP时:
中控的Bootloader 接收到文件块后,要马上烧录Flash
,根据烧录结果再回复Modbus请求
。这样,上位机每发出一个“Write File Record”,根据返回值就知道烧录是否成功,这样的程序结构简单。
② 在升级传感器APP时:
中控的APP接收到文件块后,要马上发给传感器
,传感器的Bootloader接收到后,也要马上烧录Flash
,根据烧录结果再回复 Modbus 请求给中控
,中控再回复给上位机
。这样,上位机每发出一个“Write File Record”,根据返回值就知道烧录是否成功,这样的程序结构简单。
对于上位机发来的启动命令,也是一样的:
① 目标是中控的话,中控回复给上位机后要马上执行这些命令
(先回复的原因是:执行重启命令的话,就无法回复了)。
② 目标是传感器的话,中控也要马上发送给传感器
,确定传感器执行命令后,再回复给上位机
。
上位机升级中控程序时,时序图如下:
上位机升级传感器程序时,时序图如下:
4.3 中控Bootloader 代码详解
中控代码为“E:\QuanCJ\IAP upgrades\h5_iap”
4.3.1 Bootloader 启动判断
读取配置信息,决定是否启动APP:
① 如果配置信息无效,则保持在Bootloader
② 如果配置信息明确表示要启动APP,则启动APP
代码在“h5_iap\demo\Core\Src\main.c”,如下:
启动APP的关键在于:
① 设置VTOR寄存器,重新指定异常向量表的位置为APP在Flash上的位置
② 读取APP异常向量表的第1个数据,把它写入SP寄存器
③ 读取APP异常向量表的第2个数据,跳转执行
④ 注释掉APP里设置VTOR的代码(它默认使用地址0)
前3点代码在“h5_iap\demo\Core\Src\jump.S”,如下:
4.3.2 处理启动命令
代码在“h5_iap\demo\Core\Src\control.c”,如下:
“process_emergency_cmd”函数在“h5_iap\demo\Core\Src\control.c”,代码如下:
怎么启动 Bootloader 呢?先写配置信息,再软件复位。代码在“h5_iap\demo\Core\Src\bootloader.c”:
/*********************************************************************** 函数名称: ResetToBootloader* 功能描述: 设置固件信息为"需要进入BootLoader",并软复位以便进入bootloader* 输入参数: 无* 输出参数: 无* 返 回 值: 无***********************************************************************/
void ResetToBootloader(void)
{FirmwareInfo tFirmwareInfo;memset(&tFirmwareInfo, 0xff, sizeof(FirmwareInfo));GetLocalFirmwareInfo(&tFirmwareInfo);tFirmwareInfo.bEnterBootloader = 1;WriteFirmwareInfo(&tFirmwareInfo);SoftReset();
}
怎么启动APP呢?先写配置信息,再软件复位。代码在“h5_iap\demo\Core\Src\bootloader.c”:
/*********************************************************************** 函数名称: ResetToApplication* 功能描述: 设置固件信息为"无需进入BootLoader",并软复位以便进入app* 输入参数: 无* 输出参数: 无* 返 回 值: 无***********************************************************************/
void ResetToApplication(void)
{FirmwareInfo tFirmwareInfo;memset(&tFirmwareInfo, 0xff, sizeof(FirmwareInfo));GetLocalFirmwareInfo(&tFirmwareInfo);tFirmwareInfo.bEnterBootloader = 0;WriteFirmwareInfo(&tFirmwareInfo);SoftReset();
}
4.3.3 处理文件块(Write File Record)
代码在“h5_iap\demo\Core\Src\control.c”,如下:
“process_file_record”函数在“h5_iap\demo\Core\Src\control.c”,代码如下:
static int process_file_record(uint8_t *msg, uint16_t msg_len)
{uint16_t file_no;uint16_t channel;uint16_t dev_addr;uint16_t record_no;int err = 0;if (msg[1] == MODBUS_FC_WRITE_FILE_RECORD){file_no = ((uint16_t)msg[4]<<8) | msg[5];record_no = ((uint16_t)msg[6]<<8) | msg[7];/* file_no是16位的数值* 约定:* bit[15:12]: channel* bit[11:8] : 真正的file_no, 0-map信息,1-固件* bit[7:0] : dev_addr*/channel = (file_no >> 12) & 0xf;dev_addr = (file_no) & 0xff;file_no = (file_no >> 8) & 0xf;if (channel == 0 && file_no == FILE_NUMBER_POINT_MAP){/* 处理"点"的映射信息 */parse_map_info(msg, msg_len);}if (channel == 0 && file_no == FILE_NUMBER_FIRMWARE){/* 给主控烧写固件 */burn_firmware(msg, msg_len);}if (channel != 0 && file_no == FILE_NUMBER_FIRMWARE){/* 转发固件给传感器 */SetUpdateStatus(1);err =send_firmware_to_device(msg, msg_len);if (err){SetUpdateStatus(0);}}}return err;
}
对于“给主控烧写固件”,分为2步:
① 得到record_no为0的文件头:记录文件大小、擦除Flash
② 得到record_no不为0的文件块:烧录Flash、最后写配置信息
4.4 传感器Bootloader 代码和中控APP代码详解
Bootloader 代码为“E:\QuanCJ\IAP upgrades\f030_iap”。
中控代码为“E:\QuanCJ\IAP upgrades\h5_iap”
4.4.1 Bootloader 启动判断
读取配置信息,决定是否启动APP:
① 如果配置信息无效,则保持在Bootloader
② 如果配置信息明确表示要启动APP,则启动APP
代码在“f030_iap\demo\Core\Src\main.c”,如下:
启动APP的关键在于:
① 重定位异常向量表:
STM32F030CC 没有 VTOR 寄存器,无法通过修改它指定异常向量表
,它的异常向量表永远在地址 0
。但是可以设置一个寄存器,地址 0 映射到 Flash(0x08000000)或内存(0x20000000)
。
运行Bootloader 时,地址0默认就是映射到Flash(0x08000000),无需设置
。
Bootloader 启动 APP 时,要把地址 0 映射到内存(0x20000000)
,代码在“f030_iap\demo\Core\Src\jump.S”,如下:
② 读取APP异常向量表的第1个数据,把它写入SP寄存器
③ 读取APP异常向量表的第2个数据,跳转执行
第②③点代码在“f030_iap\demo\Core\Src\jump.S”,如下:
那么,传感器的程序就不要使用内存前面的区域,如下设置:
4.4.2 处理启动命令
代码在“f030_iap\demo\Core\Src\freertos.c”,如下:
“process_emergency_cmd”函数在“f030_iap\demo\Core\Src\control.c”,代码如下:
怎么启动 Bootloader 呢?先写配置信息,再软件复位。代码在“f030_iap\demo\Core\Src\bootloader.c”:
/*********************************************************************** 函数名称: ResetToBootloader* 功能描述: 设置固件信息为"需要进入BootLoader",并软复位以便进入bootloader* 输入参数: 无* 输出参数: 无* 返 回 值: 无***********************************************************************/
void ResetToBootloader(void)
{FirmwareInfo tFirmwareInfo;memset(&tFirmwareInfo, 0xff, sizeof(FirmwareInfo));GetLocalFirmwareInfo(&tFirmwareInfo);tFirmwareInfo.bEnterBootloader = 1;WriteFirmwareInfo(&tFirmwareInfo);SoftReset();
}
怎么启动APP 呢?先写配置信息,再软件复位。代码在“f030_iap\demo\Core\Src\bootloader.c”:
/*********************************************************************** 函数名称: ResetToApplication* 功能描述: 设置固件信息为"无需进入BootLoader",并软复位以便进入app* 输入参数: 无* 输出参数: 无* 返 回 值: 无***********************************************************************/
void ResetToApplication(void)
{FirmwareInfo tFirmwareInfo;memset(&tFirmwareInfo, 0xff, sizeof(FirmwareInfo));GetLocalFirmwareInfo(&tFirmwareInfo);tFirmwareInfo.bEnterBootloader = 0;WriteFirmwareInfo(&tFirmwareInfo);SoftReset();
}
4.4.3 处理文件块(Write File Record)
代码在“f030_iap\demo\Core\Src\freertos.c”,如下:
“process_file_record”函数在“f030_iap_iap\demo\Core\Src\control.c”,代码如下:
对于烧写固件,分为2步:
① 得到record_no为0的文件头:记录文件大小、擦除Flash
② 得到record_no不为0的文件块:烧录Flash、最后写配置信息
4.4.4 中控APP要点
中控代码为“E:\QuanCJ\IAP upgrades\h5_iap”。
升级传感器程序时,上位机发来的命令、文件,通过中控转发给传感器,传感器处理后,再回复中控,中控再回复给上位机。
整个流程非常长,需要尽快处理!要点有2个:
① 中控接收到命令、“Write File Record”时,要马上处理,等待处理结果,返回给上位机
② 中控里其他读写传感器的任务,要暂时阻塞中控马上处理收到命令、“ Write File Record ”,代码在“h5_iap\demo\Core\Src\control.c”,而不是交给其他任务来处理(读写一般寄存器时,时让其他任务处理的)。如下:
怎么阻塞其他任务?代码在“h5_iap\demo\Core\Src\control.c”,如下:
相关文章:
全场景——(十一)综合实现
文章目录 一、产品框架1.1 硬件框架1.2 设计思路1.2.1 上位机如何定位到传感器1.2.2 上位机如何读写传感器1.2.3 中控如何读写传感器1.2.4 上位机如何发送映射关系、传输固件1.2.5 上位机界面 1.3 软件框架 二、改造libmodbus 实现文件传输2.1 分析Write File Record 功能2.2 实…...
mysql系列7—Innodb的redolog
背景 本文涉及的内容较为底层,做了解即可,是以前学习《高性能Mysql》和《mysql是怎样运行的》的笔记整理所得。 redolog(后续使用redo日志表示)的核心作用是保证数据库的持久性。 在mysql系列5—Innodb的缓存中介绍过:数据和索引保存在磁盘上…...
数据表中列的完整性约束概述
文章目录 一、完整性约束概述二、设置表字段的主键约束三、设置表字段的外键约束四、设置表字段的非空约束五、设置表字段唯一约束六、设置表字段值自动增加七、设置表字段的默认值八、调整列的完整性约束 一、完整性约束概述 完整性约束条件是对字段进行限制,要求…...
深入解析 Wireshark 的 TLS 设置:应用场景与实操技巧
简述 在网络数据分析中,传输层安全(TLS)协议的流量解密和分析是一项重要的技能。Wireshark 提供了专门的设置选项,帮助用户处理 TLS 流量,例如解密会话、重组分片等。本文将详细解析上图所示的 Wireshark TLS 设置功能…...
小波与傅里叶变换在去噪效果上的对比分析-附Matlab源程序
👨🎓 博主简介:博士研究生 🔬 超级学长:超级学长实验室(提供各种程序开发、实验复现与论文指导) 📧 个人邮箱:easy_optics126.com 🕮 目 录 摘要一、…...
Tailwind CSS 实战:社交媒体信息流开发
在社交媒体的世界里,信息流就像是一条永不停歇的河流,承载着用户的分享与互动。记得在一个社交平台项目中,我们通过重新设计信息流的展示方式,让用户的平均浏览时长提升了 45%。今天,我想和大家分享如何使用 Tailwind …...
深入解析:谱分解、SVD与PCA在算法中的应用与实现
特征值分解(EVD)、奇异值分解(SVD)和主成分分析(PCA)是矩阵分解技术的三种重要形式,它们在人工智能中扮演了关键角色。随着数据维度的快速增长和信息复杂度的提升,这些技术为处理高维…...
C#编写的金鱼趣味小应用 - 开源研究系列文章
今天逛网,在GitHub中文网上发现一个源码,里面有这个金鱼小应用,于是就下载下来,根据自己的C#架构模板进行了更改,最终形成了这个例子。 1、 项目目录; 2、 源码介绍; 1) 初始化; 将样…...
Android 系统 AlarmManager 系统层深度定制
Android 系统 AlarmManager 系统层深度定制 目录 引言AlarmManager 概述AlarmManager 系统架构AlarmManager 核心代码解读AlarmManager 深度定制方法 修改 AlarmManagerService 修改定时任务调度策略增加定时任务类型定制内核层 修改定时触发精度增加定时触发类型优化定时任务…...
uniapp-vue3(下)
关联链接:uniapp-vue3(上) 文章目录 七、咸虾米壁纸项目实战7.1.咸虾米壁纸项目概述7.2.项目初始化公共目录和设计稿尺寸测量工具7.3.banner海报swiper轮播器7.4.使用swiper的纵向轮播做公告区域7.5.每日推荐滑动scroll-view布局7.6.组件具名…...
Debian-linux运维-docker安装和配置
腾讯云搭建docker官方文档:https://cloud.tencent.com/document/product/213/46000 阿里云安装Docker官方文档:https://help.aliyun.com/zh/ecs/use-cases/install-and-use-docker-on-a-linux-ecs-instance 天翼云常见docker源配置指导:htt…...
【每日学点鸿蒙知识】tensorflowlite编译、音频编码线程、沉浸式状态栏、TextArea最大字节数限制等
1、如何编译Tensorflow lite库? 之前项目基于tflite推理引擎做人脸识别的功能,鸿蒙侧如何复用tflite模型? tflite对Android和iOS本身支配了GPU支持,但是鸿蒙侧目前并没有,鸿蒙提供了自己的推理引擎,而且支…...
Windows上Git LFS的安装和使用
到Git LFS官网下载 传送门 初始化GitHub LFS和Git仓库 在仓库目录中运行: git lfs install再运行: git init跟踪大文件 git lfs track "*.zip"添加并提交文件 git add . git commit -m "Add large files"上传到我的github 配…...
嵌入式入门Day37
作业 驱动机械臂 #include <myhead.h>#define IP "192.168.124.16" #define SERPORT 8888int main(int argc, const char *argv[]) {//创建套接字int oldfd socket(AF_INET, SOCK_STREAM, 0);if (oldfd -1){perror("socket");return -1;}//连接服…...
MySQL 的事务与多版本并发控制(MVCC)的那些事
什么是事务原子性:一致性隔离性 问题1: 为什么MySQL要使用mvcc实现隔离性而不使用 锁 解决并发问题?持久性 问题2: MySQL 不是磁盘数据库吗,持久化为什么是 redo log 保证的?问题 3: redo log 储存了什么东西,持久化(崩溃恢复是怎么做的?)问题 4 : MySQL 的 bing log (二进制…...
二层交换机和三层交换机
一、交换机简述 交换机的主要功能包括物理编址、网络拓扑结构、错误校验、帧序列以及流控。交换机还具备了一些新的功能,如对VLAN(虚拟局域网)的支持、对链路汇聚的支持,甚至有的还具有防火墙的功能。 交换机除了能够连接同种类型…...
Win32汇编学习笔记01.环境配置
Win32汇编学习笔记01.环境配置-C/C基础-断点社区-专业的老牌游戏安全技术交流社区 - BpSend.net 环境配置 masm32下载 官网:http://www.masm32.com/安装 成功标志 环境配置: 将masm32下的bin目录添加到path新建include,将masm32目录下的in…...
[创业之路-232]:《华为闭环战略管理》-5-组织架构、业务架构、产品架构、技术架构、项目架构各自设计的原则是什么?
目录 一、组织架构设计原则 二、业务架构设计原则 三、产品架构设计原则 四、技术架构设计原则 五、项目架构设计原则 一、各自的组成元素 组织架构、业务架构、产品架构、技术架构、项目架构各自的组成元素具体如下: 组织架构 - 组织企业相似资源的方式&…...
数组方法 | vue修改数组
数组方法 修改原数组 push() 方法(在数组结尾处)向数组添加一个新的元素 var list["数学","历史"]; list.push("英语"); ["数学","历史","英语"]unshift() 方法(在开头&…...
tcp_rcv_synsent_state_process函数
tcp_rcv_synsent_state_process 是 Linux Kernel 中用于处理 TCP 连接在 SYN-SENT 状态下接收到报文的函数。这个函数在 TCP 三次握手阶段起到了至关重要的作用,处理了在客户端发送 SYN 请求之后收到服务器响应报文的各种情况。 以下是这个函数的解读和剖析: int tcp_rcv_sy…...
【Qt】信号和槽机制
目录 1.信号和槽的理解 Qt内置类的继承关系 2.connect方法 参数 使用示例 2.1 disconnect断开信号槽 2.2 查看内置类型的信号和槽 3.自定义槽函数 3.1 代码方式自定义槽函数 3.2 图形化方式自定义槽函数 4.自定义信号 5.带参数的信号和槽 6.信号和槽的关联关系 7…...
【Pytorch实用教程】循环神经网络中使用dropout需要注意的问题
文章目录 问题解答警告的具体含义解决方案示例代码总结问题 UserWarning: dropout option adds dropout after all but last recurrent layer, so non-zero dropout expects num_layers greater than 1, but got dropout=0.3 and num_layers=1 warnings.warn("dropout op…...
展望2025:在创新与协作中创造价值、奉献佳作
2025,就像远方闪耀着希望之光的灯塔,正逐步靠近我们的视野。在这个充满无限潜力的年份里,我们满怀壮志,立志创造更多价值,为大家呈上更多出类拔萃的作品。 往昔岁月,犹如一幅徐徐展开的长卷,上…...
秒鲨后端之MyBatis【3】自定义映射resultMap、动态SQL、MyBatis的缓存、MyBatis的逆向工程、分页插件(30000字)
这里我分享一下尚硅谷的pdf100页笔记和代码,大家可以参考学习。 笔记: 通过网盘分享的文件:MyBatis.pdf 链接: https://pan.baidu.com/s/14Iu1Zs-_5vZoRjBEjmagag?pwdyjh6 提取码: yjh6 --来自百度网盘超级会员v1的分享代码: …...
springboot之集成Elasticsearch
目录 二、Elasticsearch 是什么?三、Elasticsearch 安装四、Springboot 集成 Elasticsearch 的方式五、创建项目集成 Elasticsearch 2.创建 Spring Initializr 项目 es (3).新建实体类 User(4).新建 dao 接口类 UserRe…...
CLIP (Contrastive Language-Image Pre-training)用途及使用方法
CLIP (Contrastive Language-Image Pre-training) 是由 OpenAI 开发的多模态模型,可以同时处理图像和文本。在 Hugging Face 中使用 CLIP 模型主要有以下几个步骤和用途: 安装必要的库: pip install transformers pip install torch pip install Pillow导入所需模…...
2018年西部数学奥林匹克几何试题
2018G1 未完待续… 2018 G2 在 △ A B C \triangle ABC △ABC 中, E E E, F F F 分别在 A B AB AB, A C AC AC 上, 且 B F C E B C BFCEBC BFCEBC. I B I_B IB, I C I_C IC 分别为 ∠ A B C \angle ABC ∠ABC 和 ∠ A C B \angle ACB ∠ACB 所对的旁心, K K…...
华为配置 之 链路聚合
简介: 链路聚合(Link Aggregation)是一种计算机网络技术,通过将多个物理端口汇聚在一起,形成一个逻辑端口,以实现出/入流量吞吐量在各成员端口的负荷分担。当交换机检测到其中一个成员端口的链路发生故障时…...
MIT线性代数教材:Linear Algebra and Its Applications
这本教材是MIT线性代数课程所使用的教材,上课的老师是Gilbert Strang,而教材的作者也是Gilbert Strang。这本书内容比较直观,配图不少,叙述风格比较几何风格。习题也丰富,但并不怎么对我的胃口,因此我也怎么…...
SpringBoot 集成 Activiti 7 工作流引擎
一. 版本信息 IntelliJ IDEA 2023.3.6JDK 17Activiti 7 二. IDEA依赖插件安装 安装BPM流程图插件,如果IDEA的版本超过2020,则不支持actiBPM插件。我的IDEA是2023版本我装的是 Activiti BPMN visualizer 插件。 在Plugins 搜索 Activiti BPMN visualizer 安装 创…...
【数据结构】数据结构简要介绍
数据结构是计算机科学中用于组织、管理和存储数据的方式,以便于高效地访问和修改数据。 数据结构的分类: 数据结构可以大致分为两类:线性结构和非线性结构。 1. 线性结构 线性结构中的数据按顺序排列,每个元素有唯一的前驱和后…...
SQL Server导出和导入可选的数据库表和数据,以sql脚本形式
一、导出 1. 打开SQL Server Management Studio,在需要导出表的数据库上单击右键 → 任务 → 生成脚本 2. 在生成脚本的窗口中单击进入下一步 3. 如果只需要导出部分表,则选择第二项**“选择具体的数据库对象(Select specific database objects)”**&am…...
蓝桥杯JAVA刷题--001
文章目录 题目需求2.代码3.总结 题目需求 2.代码 class Solution {public String convertDateToBinary(String date) {if (date null || date.length() ! 10 || date.charAt(4) ! - || date.charAt(7) ! -) {throw new IllegalArgumentException("输入的日期格式不正确&…...
2025-01-01 NO2. XRHands 介绍
文章目录 软件配置1 XR Hands 简介2 XRHand2.1 Pose2.2 Handedness 3 XRHandJoint3.1 XRHandJointID3.2 XRHandJointTrackingState 4 XRHandSubsystem4.1 数据属性4.1.1 UpdateSuccessFlags4.1.2 UpdateType 4.2 处理器管理:注册和注销4.3 更新手部数据:…...
SQL 实战:复杂数据去重与唯一值提取
在实际开发中,数据重复是常见问题,例如用户多次登录记录、订单状态重复更新等。如何高效提取符合业务需求的唯一值或最新记录,对系统性能和数据准确性至关重要。 本文将探讨如何使用 SQL 的 窗口函数、分组查询 以及 DISTINCT 实现复杂场景下…...
基于BiLSTM和随机森林回归模型的序列数据预测
本文以新冠疫情相关数据集为案例,进行新冠数量预测。(源码请留言或评论) 首先介绍相关理论概念: 序列数据特点 序列数据是人工智能和机器学习领域的重要研究对象,在多个应用领域展现出独特的特征。这种数据类型的核心特点是 元素之间的顺序至关重要 ,反映了数据内在的时…...
基于 SensitiveWordBs 实现敏感词过滤功能
在现代的互联网应用中,敏感词过滤已成为一个必不可少的功能,尤其是在社交媒体、评论审核等需要保证内容健康的场景下。本文将基于开源库https://github.com/houbb/sensitive-word,详细讲解如何通过自定义敏感词库和工具类实现高效的敏感词过滤…...
计算机的错误计算(一百九十八)
摘要 用两个大模型计算 arctan(54.321). 结果保留 16位有效数字。第一个大模型化简有误差;第二个大模型 Python代码几乎完全正确。无论如何,它们的结果均只有 4位数字正确。 例1. 计算 arctan(54.321). 结果保留 16位有效数字。 下面是一个大模型的回…...
递归算法.
本节我们先来了解一下递归算法. 递归算法的基本原理: 说到递归算法,就不得不提到栈.当程序执行到递归函数的时候,将函数进行入栈操作,在入栈之前,通常需要完成3件事. 1.将所有实参,返回地址等信息传递给被调函数储存 2.为被调函数的局部变量分配储存区 3.将控制转移到被调函…...
我的Java-Web进阶--SpringMVC
1.三层架构与MVC模式 三层架构 MVC模式 2.SpringMVC执行流程 3.SpringMVC的基本使用方法 1. 配置 1.1 Maven依赖 首先,在pom.xml文件中添加Spring MVC的依赖: <dependencies><!-- Spring MVC --><dependency><groupId>org.…...
【复刻】ESG表现对企业价值的影响机制研究(2009-2021年)
一、数据来源:ESG数据采用华证ESG评价体系提供的评级结果,控制变量主要来自上市公司年报,内含原始数据、处理代码和基准回归 二、数据指标:资产收益率 净利润 / 平均总资产销售净利率 净利润 / 营业收入托宾Q值 …...
GSM PDU解码在Linux下的C语言实现
GSM PDU解码在Linux下的C语言实现 一、引言二、GSM PDU格式概述三、Linux环境下的C语言实现(一)头文件包含(二)数据结构定义(三)解码函数实现(四)主函数示例四、编译与运行五、注意事项与优化六、结论一、引言 GSM(全球移动通信系统)PDU(协议数据单元)是用于在GSM…...
Vue 3.0 中 template 多个根元素警告问题
在 Vue 2.0 中,template 只允许存在一个根元素,但是这种情况在 Vue 3.0 里发生了一些变化。 在 Vue 3.0 中开始支持 template 存在多个根元素了。但是因为 VSCode 中的一些插件没有及时更新,所以当你在 template 中写入多个根元素时…...
STM32F103RCT6学习之三:串口
1.串口基础 2.串口发送 1)基本配置 注意:实现串口通信功能需在keil中设置打开Use Micro LIB,才能通过串口助手观察到串口信息 2)编辑代码 int main(void) {/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration-------------…...
07-计算机网络面试实战
07-计算机网络面试实战 计算机网络面试实战 为什么要学习网络相关知识? 对于好一些的公司,计算机基础的内容是肯定要面的,尤其是 30k 以内的工程师,因为目前处于的这个级别肯定是要去写项目的,还没上升到去设计架构的高…...
Kafka的acks机制和ISR列表
Kafka 是一个流行的分布式流处理平台,用于构建实时数据流管道和应用程序。在 Kafka 中,acks 机制和 ISR(In-Sync Replicas)列表是两个重要的概念,它们共同确保消息的持久性和可靠性。 acks 机制 acks 机制是 Kafka 生…...
c++Qt登录页面设计
使用手动连接,将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中,在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中,在槽函数中判断ui界面上输入的账号是否为"admin",密码是否为…...
数字图像处理 四 图像统计
1.直方图 记录每一种像素值出现的次数 各种直方图的类型 暗图像:分布靠低值区域 亮图像:分布靠高值区域 高对比度图像,直方图分布均匀,更容易人眼观察 2.直方图的均衡化 将低对比度图像转换为高对比度图像 视觉良好的直方图…...
UE蓝图类调用关卡蓝图中的函数
蓝图类调用关卡蓝图中函数 需要用到Execute Console Command函数节点 ce空格【函数名】 在关卡蓝图中创建一个函数sayhello 在第三人称蓝图类中调用 成功输出 注:用此方法只能从蓝图类中调用关卡蓝图中的函数,从关卡蓝图调用蓝图类是无效的。 另外Exec…...
JAVA: 状态模式(State Pattern)的技术指南
1、简述 状态模式是一种行为型设计模式,允许对象在其内部状态改变时改变其行为。它将状态相关的行为抽取到独立的状态类中,使得增加新状态变得简单,且不影响其他状态。 设计模式样例:https://gitee.com/lhdxhl/design-pattern-example.git 本文将详细介绍状态模式的概念…...