STM32关于使用定时器来实现串口通信的整活实验
目錄
- 一、整活說明
- 二、原理簡(jiǎn)介
- 先說接收
- 發(fā)送數(shù)據(jù)
- 優(yōu)化
- 三、實(shí)驗(yàn)條件和目標(biāo)
- 條件
- 目標(biāo)
- 四、掉頭發(fā)環(huán)節(jié)
- 1.定義串口類
- 2.定義簡(jiǎn)單宏
- 3.定義私有數(shù)據(jù)
- 4.init 函數(shù)
- 5.其他接口函數(shù)
- 6.中斷函數(shù)
- 7.接口導(dǎo)出
- 五、驗(yàn)證
一、整活說明
在幫別人做項(xiàng)目的時(shí)候,寫著代碼唱著歌,突然就遇到硬件工程師把線連錯(cuò)了,串口模塊連接的不是UART引腳,項(xiàng)目就這樣暫停了嗎?這能忍嗎????這不能忍啊!作為一個(gè)資深(咸魚)軟件工程師務(wù)必不能被這種小事絆住手腳,項(xiàng)目還是要進(jìn)行下去,所以就有了這次整活。
二、原理簡(jiǎn)介
串口原理就不重復(fù)了,直接網(wǎng)上隨便一篇文章就能解決問題。在此說明使用定時(shí)器實(shí)現(xiàn)串口的理論依據(jù),本篇所說的串口參數(shù)始終為8位數(shù)據(jù)位、1位停止位、無校驗(yàn),并稱傳輸一個(gè)bit所需的時(shí)間為一個(gè)tick,1 tick=(1000000/波特率)us。
先說接收
串口通信始終以一個(gè)下降沿開始維持一個(gè)tick的時(shí)間,然后開始以低位在前的形式發(fā)送數(shù)據(jù),根據(jù)數(shù)據(jù)位來控制引腳電平,持續(xù)8 tick,最后是1 tick的高電平結(jié)束。由此可得出一個(gè)方案,中斷設(shè)置為下降沿觸發(fā),然后打開定時(shí)器,每隔 1 tick 進(jìn)入一次中斷判斷接收引腳的電平并存儲(chǔ),經(jīng)過8 tick之后接收完畢,關(guān)閉定時(shí)器。
發(fā)送數(shù)據(jù)
由于串口通信的開始位和結(jié)束位都為 1 tick ,所以可以把這兩個(gè)過程看作各是1bit數(shù)據(jù)。最終要發(fā)送的數(shù)據(jù)被組合為 tdr=(data<<1)|0x40,然后打開定時(shí)器,每隔 1 tick進(jìn)入一次中斷根據(jù)tdr最低位的值控制發(fā)送引腳的電平,經(jīng)過10 tick之后發(fā)送完畢,關(guān)閉定時(shí)器。
優(yōu)化
按照上面的思路,要實(shí)現(xiàn)一個(gè)雙工串口豈不是要用2個(gè)定時(shí)器?這樣做可以是可以,但是太大材小用了。作為一個(gè)愛兵如子的軟件工程師,每個(gè)單片機(jī)外設(shè)都是極其重要的,如果一個(gè)外設(shè)不能發(fā)揮出它應(yīng)有的光芒,想必作為一個(gè)外設(shè),它也是不高興的吧。實(shí)際實(shí)現(xiàn)用的是定時(shí)器的比較輸出功能,理論上一個(gè)4通道定時(shí)器可以實(shí)現(xiàn)兩路雙工串口,只能說定時(shí)器YYSD,單片機(jī)不能沒有定時(shí)器,就像西方不能沒有耶路撒冷!
另外,由于串口通信需要頻繁進(jìn)中斷并在中斷中執(zhí)行代碼,為節(jié)省時(shí)間實(shí)際實(shí)現(xiàn)使用宏和寄存器的形式編程。
三、實(shí)驗(yàn)條件和目標(biāo)
條件
本實(shí)驗(yàn)使用的硬件使用情況如下:
| 單片機(jī) | STM32F407VGTx |
| UART_TX | PB10 |
| UART_RX | PB11 |
| 定時(shí)器 | TIM2 |
| 定時(shí)器通道1 | 用于發(fā)送 |
| 定時(shí)器通道2 | 用于接收 |
| 外部中斷 | 用于觸發(fā)接收 |
你說PB10和PB11就是USART3的TX和RX腳啊,我有串口,但是我不用,哎嘿,就是玩~
都用串口了還叫整活嗎?明顯不叫啊,沒這個(gè)理是不是?
目標(biāo)
| 參數(shù) | 1位停止位,無校驗(yàn) |
| 波特率 | 可配置 |
| 全雙工 | 是 |
| 數(shù)據(jù)發(fā)送 | 非阻塞,中斷發(fā)送 |
| 數(shù)據(jù)接收 | 緩沖區(qū),中斷接收 |
總之正常串口有的都要有,用戶體驗(yàn)上不能有區(qū)別。
四、掉頭發(fā)環(huán)節(jié)
新建文件 dev_vuart.h,dev_vuart.c
1.定義串口類
頭文件 dev_vuart.h ,添加內(nèi)容如下:
#ifndef dev_vuart_h__ #define dev_vuart_h__#include "stdint.h"typedef struct{int (*init)(void);int (*write)(const uint8_t *d,int len);int (*read)(uint8_t *d,int len);void *(*set_irqfun)(void (*fun)(uint8_t d,void *context),void *context); }vuart_typedef;vuart_typedef *vuart(void);#endif和普通串口接口相同,分為初始化,發(fā)送,接收,定義中斷函數(shù)幾個(gè)函數(shù)。
2.定義簡(jiǎn)單宏
串口需要頻繁進(jìn)入中斷并在中斷中處理數(shù)據(jù),所以不宜使用庫函數(shù)編程,定義簡(jiǎn)單宏如下,多數(shù)宏是根據(jù)stm32 hal庫修改而來。
#define __MY_TIM TIM2 #define TIM_CNT_MAX 0xffff/*r{ 定義定時(shí)器運(yùn)行頻率,單位Mhz }c*/ #define TIM_FREQUENCY (84) /*r{ 定義波特率 }c*/ #define VUART_RATE (115200)#define VUART_BIT_TICK (TIM_FREQUENCY*(1000000/VUART_RATE))#define __MY_TIM_ENABLE(__HANDLE__) ((__HANDLE__)->CR1|=(TIM_CR1_CEN))#define __MY_TIM_DISABLE(__HANDLE__) \do { \if (((__HANDLE__)->CCER & TIM_CCER_CCxE_MASK) == 0UL) \{ \if(((__HANDLE__)->CCER & TIM_CCER_CCxNE_MASK) == 0UL) \{ \(__HANDLE__)->CR1 &= ~(TIM_CR1_CEN); \} \} \} while(0)#define __MY_TIM_ENABLE_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->DIER |= (__INTERRUPT__))#define __MY_TIM_DISABLE_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->DIER &= ~(__INTERRUPT__))#define __MY_TIM_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->SR &(__FLAG__)) == (__FLAG__))#define __MY_TIM_CLEAR_FLAG(__HANDLE__, __FLAG__) ((__HANDLE__)->SR = ~(__FLAG__))#define __MY_TIM_CLEAR_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->SR = ~(__INTERRUPT__))#define __MY_TIM_GET_COUNTER(__HANDLE__) ((__HANDLE__)->CNT)#define __MY_TIM_GET_IT_SOURCE(__HANDLE__, __INTERRUPT__) (((__HANDLE__)->DIER & (__INTERRUPT__))== (__INTERRUPT__))/*r{ 設(shè)置通道寄存器值,__CHANNEL__取值0~3 }c*/ #define __MY_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \(*((&(__HANDLE__)->CCR1)+(__CHANNEL__)) = (__COMPARE__))/*r{ 獲取通道寄存器值,__CHANNEL__取值0~3 }c*/ #define __MY_TIM_GET_COMPARE(__HANDLE__, __CHANNEL__) \(*((&(__HANDLE__)->CCR1)+(__CHANNEL__)))#define __MY_SET_PIN(__GPIO__,__PIN__) (__GPIO__->BSRR = __PIN__) #define __MY_RESET_PIN(__GPIO__,__PIN__) (__GPIO__->BSRR = (uint32_t)__PIN__ << 16U)#define __MY_READ_PIN(__GPIO__,__PIN__) (__GPIO__->IDR & __PIN__)/*r{ 打開外部中斷;__INTERRUPT__取值0~22 }c*/ #define __MY_EXIT_ENABLE_IT(__INTERRUPT__) (EXTI->IMR |= 1<<(__INTERRUPT__))#define __MY_EXIT_DISABLE_IT(__INTERRUPT__) (EXTI->IMR &= ~(1<<(__INTERRUPT__)))3.定義私有數(shù)據(jù)
typedef struct {int inited;// 是否初始化uint16_t tdr;// 模擬發(fā)送寄存器uint16_t rdr;// 模擬接收寄存器uint8_t tdr_left_bit;// 發(fā)送寄存器剩余bit數(shù)uint8_t rdr_left_bit;// 接收寄存器剩余bit數(shù)data_buff sbuff;// 發(fā)送緩沖區(qū)data_buff rbuff;// 接收緩沖區(qū)void (*irqfun)(uint8_t d,void *context);// 用戶中斷回調(diào)void *context;int in_send;// 正在發(fā)送 }self_data;static self_data g_data;以及一些操作宏:
// 開始接收 #define __VUART_RX_DR()\{g_data.rdr=0;\g_data.rdr_left_bit=8;\set_channel_after_tick(1,VUART_BIT_TICK*3/2);}// 接收一個(gè)bit #define __VUART_RX_BIT()\{\if(g_data.rdr_left_bit>0)\{\if(__MY_READ_PIN(GPIOB,GPIO_PIN_11))\{\g_data.rdr|=1<<(8-g_data.rdr_left_bit);\}\g_data.rdr_left_bit--;\}\set_channel_after_tick(1,VUART_BIT_TICK);\} // 開始發(fā)送 #define __VUART_SET_DR(d)\{g_data.tdr=((uint16_t)(d)<<1)|0x0200;\g_data.tdr_left_bit=1+8+1;\set_channel_after_tick(0,VUART_BIT_TICK);}// 發(fā)送一個(gè)bit #define __VUART_TX_BIT()\{\if(g_data.tdr_left_bit)\{\if(g_data.tdr&0x0001)\__MY_SET_PIN(GPIOB,GPIO_PIN_10);\else\__MY_RESET_PIN(GPIOB,GPIO_PIN_10);\g_data.tdr>>=1;g_data.tdr_left_bit--;\}\set_channel_after_tick(0,VUART_BIT_TICK);\}函數(shù) set_channel_after_tick 的作用是設(shè)置通道在指定tick之后產(chǎn)生中斷。
/**r{* fun: 把比較寄存器值設(shè)置為tick時(shí)間之后* par: channel:0~3* return: }c**/ static void set_channel_after_tick(int channel,int tick) {int t1=__MY_TIM_GET_COUNTER(__MY_TIM);if(t1+tick>TIM_CNT_MAX)__MY_TIM_SET_COMPARE(__MY_TIM,channel,t1+tick-TIM_CNT_MAX);else__MY_TIM_SET_COMPARE(__MY_TIM,channel,t1+tick); }4.init 函數(shù)
init函數(shù)作用如下:
1.初始化緩沖區(qū)
2.初始化定時(shí)器并打開通道1,2
3.初始化引腳PB10為輸出
4.初始化引腳PB11為下降沿觸發(fā)中斷
5.打開TIM和EXIT的全局中斷
5.其他接口函數(shù)
write函數(shù)與普通串口的中斷發(fā)送操作類似,先把數(shù)據(jù)全部存入緩沖區(qū),設(shè)置發(fā)送寄存器,然后打開發(fā)送中斷即可;
read函數(shù)只需讀取緩沖區(qū)即可,與普通串口相同;
set_irqfun函數(shù)設(shè)置中斷回調(diào),也與普通串口相同。
6.中斷函數(shù)
串口發(fā)送只需要用到定時(shí)器中斷,而接收則需要同時(shí)用到外部中斷和定時(shí)器中斷。
發(fā)送中斷由write函數(shù)調(diào)用 __VUART_SET_DR ,__MY_TIM_ENABLE_IT(__MY_TIM,TIM_IT_CC1) 開啟,然后在定時(shí)器中斷中依次逐bit發(fā)送,1 byte發(fā)送完成之后檢測(cè)發(fā)送緩沖區(qū)中是否還有數(shù)據(jù),如有,則再次調(diào)用 __VUART_SET_DR 重復(fù)以上流程;如沒有數(shù)據(jù),則調(diào)用 __MY_TIM_DISABLE_IT(__MY_TIM,TIM_IT_CC1) 關(guān)閉發(fā)送中斷,此時(shí)發(fā)送結(jié)束;
接收中斷由EXTI觸發(fā),一旦檢測(cè)到下降沿,在EXTI中斷服務(wù)函數(shù)中調(diào)用 __VUART_RX_DR ,__MY_TIM_ENABLE_IT(__MY_TIM,TIM_IT_CC2),打開接收中斷,調(diào)用 __MY_EXIT_DISABLE_IT 關(guān)閉EXTI中斷防止在數(shù)據(jù)接收過程中重復(fù)觸發(fā),然后在定時(shí)器中斷中依次逐bit接收,1 byte接收完成后存入接收緩沖區(qū),然后調(diào)用 __MY_TIM_DISABLE_IT(__MY_TIM,TIM_IT_CC2) 關(guān)閉接收中斷,調(diào)用 __MY_EXIT_ENABLE_IT 打開EXTI中斷方便下次接收。
7.接口導(dǎo)出
封裝在vuart.h中定義的串口類,導(dǎo)出接口
用戶只需使用vuart()->init()來初始化,使用vuart()->write()來發(fā)送數(shù)據(jù),使用vuart()->read()來接收數(shù)據(jù)即可。
五、驗(yàn)證
在程序線程中編寫類似代碼:
void main(void){vuart()->init();while(1){vuart()->write((uint8_t *)"this msg sent by vuart.\r\n",25);while(vuart()->read((uint8_t *)&vuart_d,1))vuart()->write((uint8_t *)&vuart_d,1);delay(1000);} }經(jīng)驗(yàn)證可以實(shí)現(xiàn)數(shù)據(jù)發(fā)送和接收,但由于未經(jīng)過大量實(shí)際運(yùn)用,可能還存在一些潛在的問題,不過用整活的眼光來看已經(jīng)成功了。
總結(jié)
以上是生活随笔為你收集整理的STM32关于使用定时器来实现串口通信的整活实验的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux 中文字体美化,美化ubunt
- 下一篇: 计算机指令集的相关概念