STM32學習之旅④ USART串口和上位機通信



目錄:

一、認識其本質


(一)串口

  • 串口是串行接口 (Serial Interface)的簡稱,它是指數據一位一位地順序傳送,其特點是通信線路簡單,只要一對傳輸線就可以實現雙向通信(可以直接利用電話線作爲傳輸線),從而大大降低了成本,特別適用於遠距離通信,但傳送速度較慢。一條信息的各位數據被逐位按順序傳送的通訊方式稱爲串行通訊。串行通訊的特點是:數據位的傳送,按位順序進行,最少只需一根傳輸線即可完成;成本低但傳送速度慢。串行通訊的距離可以從幾米到幾千米;根據信息的傳送方向,串行通訊可以進一步分爲單工、半雙工和全雙工三種。

(二)協議

  • 所謂協議,就是通信雙方約定好的規定,通信雙方只有遵守這個規定才能夠完成任務。舉個栗子就是周幽王烽火戲諸侯,雙方約定好以烽火爲信號進行通信,但是愚蠢的周幽王爲博美人褒姒一笑破壞了這個規定,最後付出的代價是慘重的。可見,通信雙方只有遵守協議才能夠完成通信。

(三)時序

  • 時序就是協議的實際化,它實質上是一些列的脈衝信號,通信雙方將信息按照預先定好的規定(協議)轉換成一系列的脈衝信號,通過總線發送給接收方,接收方再將接收到的數據按照規定進行解析,從而得到發送方發送過來的數據。

(四)上位機

  • 上位機和下位機其實是一個相對的概念,上位機指的是可以直接發出操控命令的計算機,一般指PC機,能夠顯示各種信號變化(液壓,水位,溫度等),能夠將信息直接傳遞給人。下位機是直接控制設備獲取設備狀況的計算機,一般是PLC/單片機single chip microcomputer/slave computer/lower computer之類的,下位機需要PC機來對其進行控制。

二、所需材料



三、USART的介紹


  • stm32有豐富的通訊外設,USART(Universal Synchronous Asynchronous Receiver Transmitter)、SPI(Serial Peripheral interface)、I2c(Inter-Integrated Circuit)、CAN(Controller Area Network),因爲stm32有完整的且強大的固件庫,這使得配置串口的難度大大降低了,和用軟件IO口模擬通信時序相比,硬件的支持可以大大提高通信的速率、大大降低出錯的概率,從而提高了通信的質量和效率。用IO口模擬USART難度較大,它對延時要求比較苛刻,且出錯的概率較大,所以一般很少用IO口模擬USART。IO口模擬I2c比較常見,由於I2c的最高通信速度只有3.4M/s,單片機的IO口速度可以完美駕馭。由於SPI多用於一些較高速的通信,例如LCD、OLED、TFT顯示器的寫入,EEPROM (Electrically Erasable Programmable read only memory)的寫入和讀取,用IO口模擬效果不是很理想,所以建議使用硬件自帶接口。

  • 關於USART,以下是官方的介紹
    這裏寫圖片描述


四、USART串口的配置


  • 先來看一下stm32的系統結構

這裏寫圖片描述

  • 通過對stm32幾個模塊的操作,我們可以發現stm32外設配置的一些基本套路:打開相應的時鐘->配置相應的引腳功能->聲明對應的結構體->利用相應的Init函數進行初始化

  • 打開打開USATT1、GPIOA、AFIO的時鐘

void usart_config()
{
    /*打開USATT1、GPIOA、AFIO的時鐘*/
    RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 \
    | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

    /*配置對應的串口引腳*/
    usart_release_gpio_init();

    /*配置串口中斷*/
    usart_para_config();

    USART_ClearFlag(USART1,USART_FLAG_TC); //清除發送完成標誌位
    NVIC_Config();                         //初始化NVIO
    USART_Cmd(USART1, ENABLE);             //使能串口1
}
  • 配置相應的IO口,將其設爲複用推輓輸出和浮空輸入
void usart_release_gpio_init()
{
    GPIO_InitTypeDef GPIO_InitStruct;

    /*配置PA9爲複用推外輸出*/
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;

    GPIO_Init(GPIOA, &GPIO_InitStruct);

    /*配置PA10爲浮空輸入*/
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;

    GPIO_Init(GPIOA, &GPIO_InitStruct);
}

  • 配置NVIC(Nested Vectored Interrupt Controller),即內嵌向量中斷控制器,它是用來配置中斷搶佔優先級和從優先級(響應優先級)的

  • 關於搶佔優先級和響應優先級區別:

    1. 高優先級的搶佔優先級是可以打斷正在進行的低搶佔優先級中斷的。

    2. 搶佔優先級相同的中斷,高響應優先級不可以打斷低響應優先級的中斷。

    3. 搶佔優先級相同的中斷,當兩個中斷同時發生的情況下,哪個響應優先級高,哪個先執行。

    4. 如果兩個中斷的搶佔優先級和響應優先級都是一樣的話,則看哪個中斷先發生就先執行;

void NVIC_Config(void)
{
    NVIC_InitTypeDef NVIC_InitStructure; //NVIC 初始化結構體聲明

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //設置串口1 中斷
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //搶佔優先級0
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子優先級爲0
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能

    NVIC_Init(&NVIC_InitStructure);
}
  • 配置串口協議
void usart_para_config(void)
{
    USART_InitTypeDef USART_InitStruct;

    USART_InitStruct.USART_BaudRate = 115200;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;//8
    USART_InitStruct.USART_Parity = USART_Parity_No;        //N
    USART_InitStruct.USART_StopBits = USART_StopBits_1;     //1

    USART_Init(USART1, &USART_InitStruct);

    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能接收中斷
}

五、發送函數


(一)單字節發送


  • 在main函數中調用USART_SendData(USART1, 0x08);這個函數就能夠完成單字節的發送了
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
#include "usart.h"
int main()
{
    SystemInit();                //初始化系統,系統時鐘設定爲72MHz
    systick_init(72);            //配置systick,中斷時間設置爲72000/72000000 = 1us
    usart_config();
    while(1)
    {
        USART_SendData(USART1, 0x08);
        delay_ms(100);
    }
}
  • 打開串口助手就能夠看到串口發來的數據了

這裏寫圖片描述


(二)數據流發送

  • 數據流簡單來說就是一串連續的信息序列,一串序列中有若干個字節,每個字節分別對應着通信雙方預先約定好的數據含義,例如第一位代表地址、第二位代表數據流向、最後一位代表結束標誌、其餘位代表數據。數據流的長度可長可短,由通信雙方確定,但通信的過程中不能夠變化。

  • 定義一個協議棧
typedef struct
{
    u8 head;
    u8 tail;
    u8 direction;
    u8 data[4];
}send_stack;
send_stack tx_stack;
void tx_stack_init()
{
    tx_stack.head = 0xaa;     //協議棧頭,起始位,1010 1010b
    tx_stack.direction = 0x09;//數據流方向,0x09表示從單片機發出
    memset(tx_stack.data, 0, sizeof(tx_stack.data));//把tx_stack.data[]全部初始化爲零
    tx_stack.tail = 0xdd;     //協議棧尾,結束位,1101 1101b,棧頭和棧尾最好能互補
}
  • 將協議棧內的數據依次發出
void usart_senddata()
{
    u8 i;
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, tx_stack.head);
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, tx_stack.direction);
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    for(i = 0; i < 4; i++)
    {
        USART_SendData(USART1, tx_stack.data[i]);
        while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    }
    USART_SendData(USART1, tx_stack.tail);
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
  • 打開串口助手可以看到串口發來的數據流

這裏寫圖片描述



六、接收函數


  • 接收函數和發送函數類似,先定義接收協議棧
typedef struct
{
    u8 head;
    u8 tail;
    u8 direction;
    u8 data[4];
    u8 lock_flag;
    u8 data_pt;
}receive_stack;
receive_stack rx_stack;
void rx_stack_init()
{
    rx_stack.head = 0x00;         //協議棧頭,起始位
    rx_stack.direction = 0x00;    //數據流方向,0x09表示從單片機發出
    memset(rx_stack.data, 0, sizeof(rx_stack.data));//把tx_stack.data[]全部初始化爲零
    rx_stack.tail = 0x00;         //協議棧尾,結束位
    rx_stack.data_pt = 0x00;
    rx_stack.lock_flag = UNLOCK;
}
  • 接收數據需要藉助中斷來完成
void USART1_IRQHandler(void)
{
    u8 receive_data;
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //判斷讀寄存器是否非空
    {
        receive_data = USART_ReceiveData(USART1);         //接收單個字節的串口數據
        if(rx_stack.lock_flag == UNLOCK)                   //如果接收協議棧未鎖柱
        {
            if(receive_data == 0xaa)
            {
                rx_stack.head = receive_data;
            }
            else if(receive_data == 0xf9)
            {
                rx_stack.direction = receive_data;
            }
            else if(receive_data == 0xdd)
            {
                rx_stack.tail = receive_data;
                if(rx_stack.data_pt >= 4)// && (rx_stack.tail == 0xdd))
                {
                    rx_stack.data_pt = 0;
                    rx_stack.lock_flag = LOCK;
                }
            }
            else
            {
                rx_stack.data[rx_stack.data_pt] = receive_data;
                rx_stack.data_pt++;
                if(rx_stack.data_pt > 4)// && (rx_stack.tail == 0xdd))
                {
                    rx_stack.data_pt = 0;
                    rx_stack.lock_flag = LOCK;
                }
            }
        }
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);//清除接受中斷標誌
    }
}
  • 將接收到的數據流進行解析,用燈的亮滅將控制命令現實化
void ptr_handle(u8 *stack)
{
    u8 *stack_pt;
    stack_pt = stack;
    if(*stack_pt == 0xff)
    {
        key0.key_change_bit = CHGE_IN;
        if((key0.led_on_off % 2) == 1)
        {

        }
        else
        {
            key0.led_on_off = key0.led_on_off >=3 ? 0 : key0.led_on_off + 1;
        }
    }
    else
    {
        key0.key_change_bit = CHGE_IN;
        if((key0.led_on_off % 2) == 1)
        {
            key0.led_on_off = key0.led_on_off >=3 ? 0 : key0.led_on_off + 1;
        }
        else
        {

        }
    }
    stack_pt ++;
    if(*stack_pt == 0xff)
    {
        key1.key_change_bit = CHGE_IN;
        if((key1.led_on_off % 2) == 1)
        {

        }
        else
        {
            key1.led_on_off = key1.led_on_off >=3 ? 0 : key1.led_on_off + 1;
        }
    }
    else
    {
        key1.key_change_bit = CHGE_IN;
        if((key1.led_on_off % 2) == 1)
        {
            key1.led_on_off = key1.led_on_off >=3 ? 0 : key1.led_on_off + 1;
        }
        else
        {

        }
    }
    stack_pt ++;
    if(*stack_pt == 0xff)
    {
        key2.key_change_bit = CHGE_IN;
        if((key2.led_on_off % 2) == 1)
        {

        }
        else
        {
            key2.led_on_off = key2.led_on_off >=3 ? 0 : key2.led_on_off + 1;
        }
    }
    else
    {
        key2.key_change_bit = CHGE_IN;
        if((key2.led_on_off % 2) == 1)
        {
            key2.led_on_off = key2.led_on_off >=3 ? 0 : key2.led_on_off + 1;
        }
        else
        {

        }
    }
    stack_pt ++;
    if(*stack_pt == 0xff)
    {
        key3.key_change_bit = CHGE_IN;
        if((key3.led_on_off % 2) == 1)
        {

        }
        else
        {
            key3.led_on_off = key3.led_on_off >=3 ? 0 : key3.led_on_off + 1;
        }
    }
    else
    {
        key3.key_change_bit = CHGE_IN;
        if((key3.led_on_off % 2) == 1)
        {
            key3.led_on_off = key3.led_on_off >=3 ? 0 : key3.led_on_off + 1;
        }
        else
        {

        }
    }
    rx_stack.lock_flag = UNLOCK;
}
  • 主函數要做的,就是循環判斷是否有燈的狀態需要改變,每次接收到上位機發來的命令後把當前的狀態發送到上位機
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
#include "gpio_config.h"
#include "usart.h"
int main()
{
    SystemInit();                //初始化系統,系統時鐘設定爲72MHz
    systick_init(72);            //配置systick,中斷時間設置爲72000/72000000 = 1us
    led_gpio_init();
    usart_config();
    tx_stack_init();
    while(1)
    {
        if (key0.key_change_bit == CHGE_IN)
        {
            if((key0.led_on_off % 2) == 1)
            {
                LED0_ON;                      //打開LED0
                tx_stack.data[0] = 0xff;
            }
            else
            {
                LED0_OFF;                     //關閉LED0
                tx_stack.data[0] = 0x00;
            }
            key0.key_change_bit = NO_CHGE;
        }
        if (key1.key_change_bit == CHGE_IN)
        {
            if((key1.led_on_off % 2) == 1)
            {
                LED1_ON;                      //打開LED1
                tx_stack.data[1] = 0xff;
            }
            else
            {
                LED1_OFF;                     //關閉LED1
                tx_stack.data[1] = 0x00;
            }
            key1.key_change_bit = NO_CHGE;
        }
        if (key2.key_change_bit == CHGE_IN)
        {
            if((key2.led_on_off % 2) == 1)
            {
                LED2_ON;                      //打開LED2
                tx_stack.data[2] = 0xff;
            }
            else
            {
                LED2_OFF;                     //關閉LED2
                tx_stack.data[2] = 0x00;
            }
            key2.key_change_bit = NO_CHGE;
        }
        if (key3.key_change_bit == CHGE_IN)
        {
            if((key3.led_on_off % 2) == 1)
            {
                LED3_ON;                      //打開LED3
                tx_stack.data[3] = 0xff;
            }
            else
            {
                LED3_OFF;                     //關閉LED3
                tx_stack.data[3] = 0x00;
            }
            key3.key_change_bit = NO_CHGE;
            usart_senddata();
        }
        if(rx_stack.lock_flag == LOCK)
        {
            ptr_handle(rx_stack.data);       //運行協議解析函數
        }
    }
}

七、串口打印,重定向printf函數


  • 使用printf函數,需要包含其頭文件stdio.h,即標準輸入輸出頭文件,std是standard的縮寫,是標準的意思;i是input,輸入;o是output,輸出;h是head,頭,頭文件的意思。
    這裏寫圖片描述

  • 然後在usart,c中編寫字符寫入函數,將格式化後的字符串依次寫入到發送總線上

int fputc(int ch, FILE *f)
{
    while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET)
    {
    }
    USART_SendData(USART1, (uint8_t) ch);
    return ch;
}
  • 在主函數中調用printf函數,實現格式化輸出
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
#include "usart.h"
int main()
{
    SystemInit();                //初始化系統,系統時鐘設定爲72MHz
    systick_init(72);            //配置systick,中斷時間設置爲72000/72000000 = 1us
    led_gpio_init();
    usart_config();
    tx_stack_init();
    while(1)
    {
        printf("Hello World!\n%d\n\n", 12345);
        delay_ms(100);
    }
}

這裏寫圖片描述

  • 再來一個

這裏寫圖片描述


回到頂部

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章