stm32项目(3)——log_node
关于log_node节点的代码,因为一些原因变的很简单了我们先说一下当前的代码,等之后在扩展一下,最开始想要实现的效果。
思路分析
核心功能
该节点现在承担的工作只有CAN的数据收发,只会接收两种数据帧,一种是来自lp_node的电器状态帧,一个是来自host_node的时间校验帧。而自己发送只需要周期性的发送一个时间帧同步一下系统的时间,所以这部分就还是很简单,这是我第二个完成的节点,所以我基本上只是复制过来的host_node节点的代码,然后删删改改。以下是这部分的两个任务
// 处理CAN接收数据
// host -> 时间帧同步 log -> 时间校验帧 电器状态帧 lp -> 控制帧 时间帧
uint8_t led = 5;
uint8_t servo = 5;
void Task_CANReceive(void *arg){
while(1){
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
/* 调试用
format_string(CAN_rbuffer, 64,"StdID:%lu \nExtId: %lu \nIDE:%u \nRTR:%u \nDLC:%u \nData:%s \nFMI:%u",
can_rxstruct.StdId, can_rxstruct.ExtId, can_rxstruct.IDE, can_rxstruct.RTR, can_rxstruct.DLC
,can_rxstruct.Data, can_rxstruct.FMI);
*/
if(can_rxstruct.StdId == 0x333){
strncpy((char *)timp.ts_8, (char *)can_rxstruct.Data, 4);
RTC_SetCounter(timp.ts);
RTC_WaitForLastTask();
}
else if(can_rxstruct.StdId == 0x444){
led = can_rxstruct.Data[3];
servo = can_rxstruct.Data[7];
}
OLED_ShowString(4,1,"Receive");
}
}
}
// CAN发送数据任务
void Task_CANSend(void *arg){
CanTxMsg sendStruct[] = {
/* StdId ExtId IDE RTR DLC Data[8] */
{0x111, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 3, {0xAA, 0xBB, 0xCC}}, // 控制帧
{0x222, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {'a', 'b', 'c', 'd'}}, // 时间帧
{0x333, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x00, 0x00, 0x00, 0x00}},// 时间校验帧
{0x444, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 8, {0x00, 0x00, 0x00, 0x00}},// 电器状态帧
};
while(1){
timp.ts = tim;
CAN_SendMsg(&sendStruct[1], (char*)timp.ts_8); // 时间帧
vTaskDelay(pdMS_TO_TICKS(10000));// 每5s校验一次
OLED_Clear();
}
}
业务功能
在这部分代码中,可以看到我定义了两个全局变量,这个就是后面修改的内容,本来打算接收电器状态,生成日志,但是硬件资源有限,只能在OLED屏幕上显示一下电器状态,充当日志了。
除此之外我们还要显示系统时间,这个部分基本上三个MCU都是一致的,所以不过多阐述
td time_and_date;
struct tm *time_info;
void Task_ShowTime(void *arg){
char buff[15];
while(1){
tim = RTC_GetCounter();
RTC_WaitForSynchro();
OLED_ShowUnsignedNum(1, 1, tim, 10);
time_info = localtime(&tim);
time_and_date.year = time_info->tm_year + 1900; // tm_year 是从 1900 年开始计算的偏移量
time_and_date.mon = time_info->tm_mon + 1; // tm_mon 是 0-11
time_and_date.day = time_info->tm_mday;
// 直接格式化时间,避免生成完整的 asctime 字符串
snprintf(time_and_date.time, sizeof(time_and_date.time), "%02d:%02d:%02d",
time_info->tm_hour, time_info->tm_min, time_info->tm_sec);
OLED_ShowUnsignedNum(2,1,time_and_date.year, 4);
OLED_ShowUnsignedNum(2,6,time_and_date.mon, 2);
OLED_ShowUnsignedNum(2,9,time_and_date.day,2);
OLED_ShowString(3,1,time_and_date.time);
format_string(buff,15, "led:%u ser:%u\0", led, servo);
OLED_ShowString(4,1,buff);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
以上就是这个部分的全部任务了,就只有三个,可以说都不需要引入FreeRTOS就可以很好的完成。如果你看源码的话,任务中还有个ADC的任务,但是没有创建该任务。因为把该功能移交到host_node了,但是我想以后我可能还会扩展该节点,所以也就没有删除。
扩展
我是因为硬件资源有限,不能很好的查看该节点的数据存储情况,所以实现的很简陋,在项目写完后,想到查看这里面数据的办法,所以下面和大家讲述一下该节点在我设计时的思路。上面演示过的就不再赘述了。
原版核心功能应该还有一个日志系统,使用W25Q64保存数据。关于这方面的实现,我们首先要先清楚,这是flash编程,写入要一页一页的写,那么就是256字节,而flash编程是先擦后写,擦除又是以扇区为单位,每个扇区是4096字字节,也就是每十六页就需要写入下一个扇区了。
这部分就是外设的操作,也就是我们的核心功能逻辑,然后基于此我们要继续盘一盘业务功能逻辑。
业务功能,那么主要就是怎么样存储日志,存储什么样的日志格式,那么就是简单的时间戳+电器状态就可以了。然后传感器数据采集任务也可以放到这里了,还可以加一个传感器数据。
格式就可以敲定了
char state_log[] = "<日期,时间> LED:LED Servo:Servo状态,"
char sensor_log[] = "<日期,时间> PA0 = 传感器数据 PA1 = 传感器数据"
char sync_log[] = "<日期,时间> sync System Time :时间戳"
基本上这三个就足够了,然后就是写入W25Q64当中,另外还有提供一个读出W25Q64的内容。
关于这部分我的思路是采用一个256字节的缓冲区来完成,刚好1页的大小,当缓冲区剩余容量不能写入下一条日志信息的时候,就把缓冲区内容写入W25Q64,这样做的好处一个是磨损均衡,另外一个是不需要麻烦的定位操作,只需要记录下一个要写入的页号就可以了,这样做擦除扇区的时机也比较好确定。尽管可能会造成一些资源浪费,但是这样操作简便了很多,我们读取数据也是直接读取一页就可以了。
// --------------W25Q64-----------------
#ifdef USE_SPI
#define SPIX SPI2
#define SPI_PORT GPIOB
#define SPI_MOSI GPIO_Pin_15
#define SPI_MISO GPIO_Pin_14
#define SPI_SCL GPIO_Pin_13
#define SPI_CSS GPIO_Pin_12
#define W25Q64_PAGESIZE 256 // 一页大小
#define W25Q64_SECTOR_SIZE 4096 // 一块大小
#define W25Q64_SECTOR_NUM 2048 // 块的数量
extern char W25Q64_buffer[]; // 设置一个数据缓冲区,进行一页数据的交互
extern uint32_t W25Q64_buffer_ptr ; // 缓冲区下一次写入位置
extern uint16_t W25Q64_current_page ; // 记录当前指针所在页
extern uint16_t total_log_num; // 记录log数量
extern uint16_t current_erase_sector; // 准备擦除块
extern uint32_t W25Q64_data_start; // 数据起始地址
extern uint32_t W25Q64_data_end; // 数据结束地址
#endif
下面是写入的一些逻辑
核心实现要有三个,一个是格式化写入,一个是判断log类型,一个是扇区擦除时机,但是因为我加入了一个缓冲区,所以在这里我还要加一个将缓冲区数据搬运到W25Q64当中
// 擦除W25Q64的下一块扇区
void vErase_W25Q64_Sector(void *arg){
while(1){
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
EraseSector((current_erase_sector % W25Q64_SECTOR_NUM) * W25Q64_SECTOR_SIZE);
current_erase_sector++;
}
}
}
// 格式化日志类型
// 在这里我考虑到以后可能会有多种日志情况所以加了一个互斥锁
// 比如出现什么报警事件,在报警任务内部实现日志,之后再同步写入W25Q64
void vfromatLog(void *arg){
while(1){
//等待锁
xSemaphoreTake(PrintLog_Mutex, portMAX_DELAY);
if(flag = 1) format_log(STATE_LOG);
else if(flag = 2)format_log(SENSOR_LOG);
else format_log(SYNC_LOG);
xTaskNotifyGive(vPrintLog_handle);
// 释放锁
xSemaphoreGive(PrintLog_Mutex);
// 10s 打印一次日志
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
// 输出log信息
void vPrintLog(void *arg){
// 释放自己的信号量
xSemaphoreGive(PrintLog_Mutex);
uint8_t len;
xTaskNotifyGive(vErase_W25Q64_Sector_handle); // 先擦除一片扇区
while(1) {
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
// 连接服务器则向服务器输出
// 计算log长度
len = strlen(log_buffer);
// 日志输入到W25Q64缓冲区
format_string(W25Q64_buffer + W25Q64_buffer_ptr , W25Q64_PAGESIZE, "%s", log_buffer);
// 更新指针
W25Q64_buffer_ptr += len;
// 日志数量
total_log_num++; // log总数+1
// 如果缓冲区不足,则将缓冲去内容输入W25Q64中(数据长度加当前指针 > 页大小)
if(len + W25Q64_buffer_ptr > W25Q64_PAGESIZE){
xTaskNotifyGive(vMovebuffer2W25Q64_handle);
W25Q64_current_page++;
}
}
}
}
// 将缓冲区数据搬运到W25Q64
void vMovebuffer2W25Q64(void *arg){
while(1){
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
W25Q64WriteStr(W25Q64_data_end, W25Q64_buffer, W25Q64_PAGESIZE);
W25Q64_data_end += W25Q64_PAGESIZE; // 更新W25Q64数据尾指针
W25Q64_buffer_ptr = 0;
// 清除缓冲区
clear_buffer(log_buffer, LOG_BUFFERSIZE);
clear_buffer(W25Q64_buffer, W25Q64_PAGESIZE);
// 判断是否需要准备新的扇区
if(((W25Q64_current_page + 1) * W25Q64_PAGESIZE > current_erase_sector * W25Q64_SECTOR_SIZE)) {
xTaskNotifyGive(vErase_W25Q64_Sector_handle);
}
}
}
}
以上基本上就是我的日志系统的逻辑了,这部分实现可能会有问题,毕竟不是结合实际写的,但是思路差不多就是这样了。这样就可以比较完善的记录我们的日志信息。
另外我们需要读取日志的话,可以通过串口相连,当前我们host_node节点只使用了USART1,引脚资源还是很富裕的,所以我们可以将log_node节点的USART1和host_node节点的USART2交叉相连,用来做一个专门的log信息通路,那么这里的实现就比较单一了,不需要很复杂的判断逻辑。
host_node:只发送一个数字就好了,代表查看第几页的日志;接收就是这一页的信息,直接用USART1输出USART2的接收缓冲区
log_node:只发送指定页的日志信息,加一个判断如果越界,那么就发送给host_ndoe一个“ERROR: the {数字}th page have no log!\n”,数据接收就是一个字符串格式的数字,转成无符号就可以了。
以上呢就是该节点的全部逻辑拆解了。在该文章中只贴了任务部分的一些代码,具体代码可以在我的gitee仓库自取,仓库地址:https://gitee.com/xingyexiakong/stm32project_-smart-green-house
更多推荐

所有评论(0)