STM32项目(2)——host_node
思路分析
核心功能
host_node节点的核心逻辑就是承担着与用户交互和其它节点进行数据交互。与用户交互,我们无疑是使用USART通信了,与其他主机进行通信我们使用CAN。
业务功能
在这里我把传感器数据采集放到了这里,所以需要用到ADC。另外还要实时的显示当前时钟,以确保三个MCU的时间是同步的,所以还需要初始化RTC。
辅助功能
DMA+IDLE和DMA+ADC的自动化采集都是可以极大的节省CPU的开销。所以我们引入DMA。采集数据后,我们是通过OLED屏幕显示,我的屏幕是I2C屏幕,所以需要用到I2C(不过这里的代码是从店铺客服哪里获取的现成的不需要我们手写)。
USART初始化实现
在这里我想要专门展示一下USART的初始化实现,这个代码是我上个项目中写的,我觉得设计的很好。主要是通过宏定义,能够比较简单的开启和关闭对应的串口和接收数据方式。在我的上一个项目,项目整体都是使用这种宏的方式来控制。最后通过一个配置文件来控制对应宏的开启,以下是代码部分仅供参考。
#include "USART.h"
#ifndef USART_DMA_RECEIVE // 定义接收数据方式
#define USART_DMA_RECEIVE 0 // 0 为中断接收数据(默认), 1为DMA接收数据
#endif
#ifndef USARTID
#define USARTID 1
#endif
#if(USARTID & 0x01)
extern char usart_buffer1[];
extern volatile uint8_t usart_buffer1_idx;
#define USART1_PORT GPIOA
#define USART1_PIN_TX GPIO_Pin_9
#define USART1_PIN_RX GPIO_Pin_10
#define USART1_PORT_CLOCK RCC_APB2Periph_GPIOA
#define USART1_CLOCK RCC_APB2Periph_USART1
#endif
#if(USARTID & 0x02)
extern char usart_buffer2[];
extern volatile uint8_t usart_buffer2_idx;
#define USART2_PORT GPIOA
#define USART2_PIN_TX GPIO_Pin_2
#define USART2_PIN_RX GPIO_Pin_3
#define USART2_PORT_CLOCK RCC_APB2Periph_GPIOA
#define USART2_CLOCK RCC_APB1Periph_USART2
#endif
#if(USARTID & 0x04)
extern char wifi_name[20];
extern char paswd[20];
extern volatile uint8_t Flag_count;
volatile static char *ptr = wifi_name;
#define USART3_PORT GPIOB
#define USART3_PIN_TX GPIO_Pin_10
#define USART3_PIN_RX GPIO_Pin_11
#define USART3_PORT_CLOCK RCC_APB2Periph_GPIOB
#define USART3_CLOCK RCC_APB1Periph_USART3
#endif
static void init_GPIO(uint32_t USART_PORT_CLOCK, uint16_t USART_PIN_TX, uint16_t USART_PIN_RX, GPIO_TypeDef* USART_PORT){
RCC_APB2PeriphClockCmd(USART_PORT_CLOCK, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStruct.GPIO_Pin = USART_PIN_RX;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(USART_PORT, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = USART_PIN_TX;
GPIO_Init(USART_PORT, &GPIO_InitStruct);
}
void init_USART(void){
#if(USARTID & 0x01)
init_GPIO(USART1_PORT_CLOCK, USART1_PIN_TX, USART1_PIN_RX, USART1_PORT);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
#endif
#if(USARTID & 0x02)
init_GPIO(USART2_PORT_CLOCK, USART2_PIN_TX, USART2_PIN_RX, USART2_PORT);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
#endif
#if(USARTID & 0x04)
init_GPIO(USART3_PORT_CLOCK, USART3_PIN_TX, USART3_PIN_RX, USART3_PORT);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
#endif
USART_InitTypeDef USART_InitStruct;
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;
#if(USARTID & 0x01)
USART_InitStruct.USART_BaudRate = 115200; // PC波特率
USART_Init(USART1, &USART_InitStruct);
USART_Cmd(USART1, ENABLE);
#if (USART_DMA_RECEIVE == 1)
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);
#endif
#endif
#if(USARTID & 0x02)
USART_InitStruct.USART_BaudRate = 115200; //wifi波特率
USART_Init(USART2, &USART_InitStruct);
USART_Cmd(USART2, ENABLE);
#if (USART_DMA_RECEIVE == 1)
USART_DMACmd(USART2,USART_DMAReq_Rx,ENABLE);
#endif
#endif
#if(USARTID & 0x04)
USART_InitStruct.USART_BaudRate = 9600; // 蓝牙波特率
USART_Init(USART3, &USART_InitStruct);
USART_Cmd(USART3, ENABLE);
#if (USART_DMA_RECEIVE == 1)
USART_DMACmd(USART3,USART_DMAReq_Rx,ENABLE);
#endif
#endif
}
void USART_SendStr(USART_TypeDef* USARTX, const char *str){
while(*str){
while(USART_GetFlagStatus(USARTX, USART_FLAG_TXE) == RESET);
USART_SendData(USARTX, *str++);
}
}
void init_USART_NVIC(void){
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 11;
#if(USARTID & 0x01)
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_Init(&NVIC_InitStruct);
#if USART_DMA_RECEIVE
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
#else
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
#endif
#endif
#if(USARTID & 0x02)
NVIC_InitStruct.NVIC_IRQChannel = USART2_IRQn;
NVIC_Init(&NVIC_InitStruct);
#if USART_DMA_RECEIVE
USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
#else
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
#endif
#endif
#if(USARTID & 0x04)
NVIC_InitStruct.NVIC_IRQChannel = USART3_IRQn;
NVIC_Init(&NVIC_InitStruct);
#if USART_DMA_RECEIVE
USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
#else
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
#endif
#endif
//__enable_irq(); // 打开CPU总中断
}
// 接收PC交互数据
#if(USARTID & 0x01)
#if(USART_DMA_RECEIVE == 1)
void USART1_IRQHandler(void){
if(USART_GetITStatus(USART1, USART_IT_IDLE) == SET){
// 清除IDLE标志位,必须遵循特定的软件读取序列
volatile uint32_t temp;
temp = USART1->SR;
temp = USART1->DR;
(void)temp;
// 计算实际接收长度
// uint16_t len = BUFFERSIZE - DMA_GetCurrDataCounter(USART1_RX_CHANNEL);
DMA_Cmd(USART1_RX_CHANNEL, DISABLE);
DMA_SetCurrDataCounter(USART1_RX_CHANNEL, BUFFERSIZE);
DMA_Cmd(USART1_RX_CHANNEL, ENABLE);
BaseType_t Flag = pdFALSE;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
vTaskNotifyGiveFromISR(Task_USART1Receive_handle, &Flag);
portYIELD_FROM_ISR(Flag);
}
}
#else
void USART1_IRQHandler(void){
if( USART_GetITStatus(USART1, USART_IT_RXNE) == SET){
char ch = USART_ReceiveData(USART1);
if(ch == '\r' || ch == '\n') {
usart_buffer1[usart_buffer1_idx] = '\0';
usart_buffer1_idx= 0;
BaseType_t Flag = pdFALSE;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
vTaskNotifyGiveFromISR( vAnalyseDataFromUSART1_handle, &Flag);
portYIELD_FROM_ISR(Flag);
}
else {
if(usart_buffer1_idx < USART_BUFFERSIZE - 1) {
usart_buffer1[usart_buffer1_idx++] = ch;
}
else {
usart_buffer1[usart_buffer1_idx] = '\0';
usart_buffer1_idx= 0;
}
}
}
}
#endif
#endif
// 接收USART2数据并处理
#if(USARTID & 0x02)
#if(USART_DMA_RECEIVE == 1)
void USART2_IRQHandler(void){
USART1_SNED_FROM flag = USART1_SEND_FROM_USART2_RECEIVE;
if(USART_GetITStatus(USART2, USART_IT_IDLE) == SET){
// 清除IDLE标志位,必须遵循特定的软件读取序列
volatile uint32_t temp;
temp = USART2->SR;
temp = USART2->DR;
(void)temp;
// 计算实际接收长度
// uint16_t len = BUFFERSIZE - DMA_GetCurrDataCounter(USART1_RX_CHANNEL);
DMA_Cmd(USART2_RX_CHANNEL, DISABLE);
DMA_SetCurrDataCounter(USART2_RX_CHANNEL, BUFFERSIZE);
DMA_Cmd(USART2_RX_CHANNEL, ENABLE);
BaseType_t Flag = pdFALSE;
xQueueSendFromISR(Task_USART1Send_QueueHandle, &flag, &Flag);
portYIELD_FROM_ISR(Flag);
}
}
#else
void USART2_IRQHandler(void){
if( USART_GetITStatus(USART2, USART_IT_RXNE) == SET){
char ch = USART_ReceiveData(USART2);
if(ch == '\r' || ch == '\n') {
//usart_buffer2[usart_buffer2_idx] = '\0';
usart_buffer2_idx= 0;
BaseType_t Flag = pdFALSE;
USART_ClearITPendingBit(USART2, USART_IT_RXNE);
vTaskNotifyGiveFromISR( vWifiSendData2PC_handle, &Flag);
portYIELD_FROM_ISR(Flag);
}
else {
if(usart_buffer2_idx < USART_BUFFERSIZE - 1) {
usart_buffer2[usart_buffer2_idx++] = ch;
}
else {
usart_buffer2[usart_buffer2_idx] = '\0';
usart_buffer2_idx= 0;
}
}
}
}
#endif
#endif
// 接收蓝牙数据并处理
#if(USARTID & 0x04)
void USART3_IRQHandler(void){
if( USART_GetITStatus(USART3, USART_IT_RXNE) == SET){
char ch = USART_ReceiveData(USART3);
switch(ch){
case '!': Flag_count++;break;
case '=': ptr = wifi_paswd;break;
default : *ptr++ = ch;
}
if(Flag_count == 2){
*ptr = '\0';
ptr = wifi_name;
BaseType_t Flag = pdFALSE;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
vTaskNotifyGiveFromISR(vBlueTooth_handle, &Flag);
portYIELD_FROM_ISR(Flag);
}
}
}
#endif
实现逻辑
拆解完功能,我们也就清楚了需要哪些外设,下面就可以进一步细化如何实现。首先我们引入了FreeRTOS系统,所以需要明确至少要有哪些任务,然后根据我们实际的业务需求再继续增加。
首先是我们的核心功能,USART的收发和CAN的收发。
USART相关实现
先以USART为例,在该项目中我们只启用了USART1与PC端相连同用户进行交互。USART接收完我们的数据,肯定是需要解析我们的指令,然后再执行或发送相应的命令,所以我们需要有一个USART数据接收后的处理任务。另外就是我们使用的是DMA+IDLE的方式,所以需要在设置好IDLE中断并在IDLE中断中唤醒该任务,为了节省开销采用任务通知的方式。所以就还需要该任务的句柄,另外就是可以对接收的数据解析封装成一个函数,但是在我这比较简单所以没有封装。这里的功能不固定,我也是随用随加的。
// USART1处理接收数据任务
void Task_USART1Receive(void *arg){
USART1_SNED_FROM flag = USART1_SNED_FROM_USART1_RECEIVE;
// 调试用
UBaseType_t uxHighWaterMark;
uint32_t ulUsedBytes;
while(1){
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
// 测试,USART2打印输入的字符串
if(!memcmp(buffer1, "text:", 5)){
xQueueSend(Task_USART1Send_QueueHandle, &flag, portMAX_DELAY);
}
// 发送时间校验帧
else if(!memcmp(buffer1, "jy:",3)){
sscanf(buffer1,"jy:%ul", &timp.ts);
timp.ts += 8 * 60 * 60;
xTaskNotifyGive(Task_CANSend_handle);
}
// 改变LED状态
else if(!memcmp(buffer1, "cmdl", 4)){
command = CON_LED;
xTaskNotifyGive(Task_CANSend_handle);
}
// 开启通风
else if(!memcmp(buffer1, "open", 4)){
command = CON_Servo_OPEN;
xTaskNotifyGive(Task_CANSend_handle);
}
// 关闭通风
else if(!memcmp(buffer1, "clos", 4)){
command = CON_Servo_CLOSE;
xTaskNotifyGive(Task_CANSend_handle);
}
// 调试功能
// 创建任务时的栈大小参数(usStackDepth)单位是字(Word),而不是字节, 128 * 4 = 512
else {
uxHighWaterMark = uxTaskGetStackHighWaterMark(Task_USART1Receive_handle);
ulUsedBytes = (configMINIMAL_STACK_SIZE - uxHighWaterMark) * sizeof(StackType_t);
format_string(buffer1, BUFFERSIZE, "Stack MAX: %lu Bytes", ulUsedBytes);
xQueueSend(Task_USART1Send_QueueHandle, &flag, portMAX_DELAY);
}
}
}
}
关于USART数据发送,从上面的代码我们看到了已经封装好了一个发送字符串的函数,又需要直接可以在任务中发送就可以了。这在小项目中不会有问题,但是如果是一个相对复杂的项目就会出现问题,不妨试想一下,一个任务正在发送100个字节的数据,发送到第80个字节的时候,突然被高优先级任务抢占,而高优先级任务也要发送数据…所以,我们需要为发送数据也设置一个任务用来管理数据发送的顺序,这里就适合用消息队列进行管理了,但是对于消息队列的设计,分享一个自己的小巧思。消息队列接收枚举类型,然后定义一个函数,对枚举类型进行匹配,将对应的缓冲区数据搬运到发送缓冲区中,这样做可能会多浪费一些空间给很多任务做缓冲区,但是这样做的同步效果也是很明显的,下面是该部分代码的核心逻辑。
// USART1发送任务
void Task_USART1Send(void *arg){
BaseType_t Ret = pdFAIL;
// 调试用 112
UBaseType_t uxHighWaterMark;
uint32_t ulUsedBytes;
while(1){
Ret = xQueueReceive(Task_USART1Send_QueueHandle, &Task_USART1Send_From, portMAX_DELAY);
if(Ret == pdPASS){
match_usart1_send_from();
}
else {
// 调试用(查看水位线)
uxHighWaterMark = uxTaskGetStackHighWaterMark(Task_USART1Receive_handle);
ulUsedBytes = (configMINIMAL_STACK_SIZE - uxHighWaterMark) * sizeof(StackType_t);
format_string(buffer_send1, BUFFERSIZE, "Stack MAX: %lu Bytes", ulUsedBytes);
}
// 发送数据
USART_SendStr(USART1, buffer_send1);
// 重置标志位
Task_USART1Send_From = USART1_SNED_FROM_NONE;
// 清空缓冲区
clear_buffer(buffer_send1, BUFFERSIZE);
}
}
// USART1Send枚举类型
typedef enum{
USART1_SNED_FROM_NONE,
USART1_SNED_FROM_USART1_RECEIVE,
USART1_SEND_FROM_CANReceive,
USART1_SEND_FROM_CHECK
}USART1_SNED_FROM;
// 标志位
extern USART1_SNED_FROM Task_USART1Send_From;
// 匹配函数
void match_usart1_send_from(void){
switch(Task_USART1Send_From){
case USART1_SNED_FROM_NONE: break;
case USART1_SNED_FROM_USART1_RECEIVE:{
format_string(buffer_send1, BUFFERSIZE, "%s", buffer1 + 5);
clear_buffer(buffer1, BUFFERSIZE);
};break;
case USART1_SEND_FROM_CANReceive:{
format_string(buffer_send1, BUFFERSIZE, "CAN Receive:%s", CAN_rbuffer);
clear_buffer(CAN_rbuffer, 64);
}break;
case USART1_SEND_FROM_CHECK:{
format_string(buffer_send1, BUFFERSIZE, "Sensor Check:%s", check_buffer);
clear_buffer(check_buffer, 32);
}break;
default:
clear_buffer(buffer1, BUFFERSIZE);
}
}
CAN相关实现
其实和USART的思路大差不差,同样需要两个任务,不同的是,CAN的数据收发比较固定,不需要太多的逻辑,因为他就是负责与节点的通信。
关于接收数据,在上一篇中我们就得值,host_node节点只会接收到时间帧同步时间,所以这个任务只是将接收的时间戳给RTC同步就可以了。
关于CAN的数据发送,在这里我也建议刚接触CAN的同学用这样的方式去写。
// 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){
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
if(command == CON_NONE){ // 时间校验帧
CAN_SendMsg(&sendStruct[2], (char*)timp.ts_8);
}
else{ // 控制帧
if(command == CON_LED) CAN_SendMsg(&sendStruct[0], "cled");
else if(command == CON_Servo_OPEN) CAN_SendMsg(&sendStruct[0], "open");
else if(command == CON_Servo_CLOSE) CAN_SendMsg(&sendStruct[0], "clos");
}
command = CON_NONE;
}
}
}
因为我的项目比较简单,帧的类型不多,我就在我所有的节点实现的代码中的这部分初始化了四种帧格式,这样就发送起来简单,另外就是不需要来回的看和翻找确认了,尽管这对于程序来讲是一个无意义且浪费资源的行为,但是可以极大的便利我们,当然注释放在这里一样可以提醒,但是这样还有另外一个好处就是,我们做环回测试的时候,可以更方便的在一个MCU配置过滤器。
以上就是核心功能的实现了!
接下来是业务功能的拆解
这里就是展示ADC的数据采集和当前的时间,因为他们都是输出到OLED屏幕上可以放到一个任务中,因为时间戳是RTC自动累加的,ADC我们也是配合DMA自动化输出的,所以这部分任务很简单,我们只需要调用相关函数输出到OLED就好了。在这里可能需要看一下C语言的time.h,我也是现看现用的,并不复杂
void Task_ShowTime(void *arg){
td time_and_date;
char sensor_data[15];
struct tm *time_info;
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(sensor_data, 15,"[%4u] [%4u]", ADC_buffer[0], ADC_buffer[1]);
OLED_ShowString(4,1,sensor_data);
vTaskDelay(pdMS_TO_TICKS(20));
}
}
除此之外我们还应该实现一个功能,那就是根据采集到的传感器数据进行相应的控制,比如温度高了就通风,温度低了就关闭通风,这里我使用的是电位器,所以就简单的用模拟量代替这个功能。另外就是需要一个注意的点。就是我们检查完一次之后应该给一些时间让控制节点去处理,不能一直去触发,而且正常应该是每隔时间检查一次,到这里可能会有些处理不好这个时间关系,那么我们可以发现异常之后不去检查,并设置一个闹钟,闹钟计时好了再开启检查。
// 检查环境
void Task_CheckEnviroment(void *arg){
uint8_t check = 1;
USART1_SNED_FROM flag = USART1_SEND_FROM_CHECK;
uint8_t trigger = 0;
while(1){
if(check){
// 过热
if(ADC_buffer[0] > 3000 || ADC_buffer[1] > 3000 ){
command = CON_Servo_OPEN;
format_string(check_buffer, 32, "Exceeds the threshold!\n");
trigger = 1;
}
// 过冷
else if(ADC_buffer[0] < 1000 || ADC_buffer[1] < 1000){
command = CON_Servo_CLOSE;
format_string(check_buffer, 32, "Below the threshold!\n");
trigger = 1;
}
else{
format_string(check_buffer, 32, "[checking]Everythinfg is normal!\n");
trigger = 0;
}
// 出现异常则控制节点转动舵机
if(trigger){
xTaskNotifyGive(Task_CANSend_handle);
// 一段时间内停止检查
check = 0;
// 设置闹钟5分钟后恢复
RTC_SetAlarm(tim + 5 * 60);
}
xQueueSend(Task_USART1Send_QueueHandle, &flag, portMAX_DELAY);
}
else{
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
// 等待闹钟唤醒
check = 1;
trigger = 0;
}
}
// 每秒检查一次
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
以上呢就是该节点的全部逻辑拆解了。在该文章中只贴了任务部分的一些代码,具体代码可以在我的gitee仓库自取,仓库地址:https://gitee.com/xingyexiakong/stm32project_-smart-green-house
更多推荐

所有评论(0)