隨著單片機(jī)的使用日益頻繁,用其作前置機(jī)進(jìn)行采集和通信也常見于各種應(yīng)用,一般是利用前置機(jī)采集各種終端數(shù)據(jù)后進(jìn)行處理、存儲,再主動(dòng)或被動(dòng)上報(bào)給管理站。這種情況下下,采集會需要一個(gè)串口,上報(bào)又需要另一個(gè)串口,這就要求單片機(jī)具有雙串口的功能,但我們知道一般的單片機(jī)只提供一個(gè)串口,那么另一個(gè)串口只能靠程序模擬。
1、串口傳輸協(xié)議
首先,必須要知道串口通訊時(shí)數(shù)據(jù)是怎樣傳輸?shù)模窟@里以異步傳輸字符為例子,如下圖所示:
一般字符傳輸都采用:1位起始位,8位數(shù)據(jù)位,1位停止位,沒有校驗(yàn)位 的形式傳輸,其他形式的這里不講。串口異步傳輸在空閑狀態(tài)時(shí)都必須是高電平。第一位傳輸?shù)氖瞧鹗嘉唬鹗嘉粫⒃瓉砜臻e時(shí)的高電平拉成低電平,起始位用來來標(biāo)識數(shù)據(jù)開始傳輸,提示接收方準(zhǔn)備開始接收數(shù)據(jù);當(dāng)接收方第一次檢測到一個(gè)下降沿時(shí),就表示接收到了起始位。起始位后就是8位的數(shù)據(jù)位,接收方在接收每一位數(shù)據(jù)的時(shí)候會采集幾十次,如果結(jié)果都是低電平,則接收到的數(shù)據(jù)位0,如果結(jié)果都是高電平,則棘手到的數(shù)據(jù)位是1。1位停止位會將電平拉成高電平,以為接收下一個(gè)數(shù)據(jù)做準(zhǔn)備。
2、IO模擬串口發(fā)送程序
IO口模擬串口發(fā)送數(shù)據(jù),必須嚴(yán)格按照上面的異步傳輸協(xié)議。我們用偽代碼實(shí)現(xiàn)這一過程:
void VirtualCOM_ByteSend(u8 val)
{
u8 i;
IO_LOW(); //起始位,拉低電平
Delay(sometime);
for(i = 0; i 《 8; i++) //8位數(shù)據(jù)位
{
if(val & 0x01)
IO_HIGH();
else
IO_LOW();
Dealy(sometime);
val 》》= 1;
}
IO_HIGH(); //停止位,拉高電平
Delay(sometime);
}
代碼很簡單,思路也很清晰,完全是按照異步傳輸?shù)倪^程寫的。這里最重要的是Delay(sometime),sometime的延時(shí)時(shí)間就決定了傳輸?shù)乃俣龋瑂ometime去取某些值才可以設(shè)置程序標(biāo)準(zhǔn)的串口波特率(1200、2400、9600、38400、115200等等)。
下面,我采用STM32開發(fā)板實(shí)現(xiàn)IO模擬串口發(fā)送。
(1)選擇IO引腳設(shè)置為虛擬串口TX引腳
我選擇PA4引腳來模擬串口的TX引腳,所以需要配置下PA4這個(gè)引腳為推挽輸出:
#define COM_TX_PORT GPIOA
#define COM_TX_PIN GPIO_Pin_4
void VirtualCOM_TX_GPIOConfig(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* PA4最為數(shù)據(jù)輸出口,模擬TX */
GPIO_InitStructure.GPIO_Pin = COM_TX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(COM_TX_PORT, &GPIO_InitStructure);
GPIO_SetBits(COM_TX_PORT, COM_TX_PIN);}
這里需要說明的是,在配置完引腳后,需要將PA4引腳拉高,這樣做是為了防止在發(fā)送數(shù)據(jù)起始位時(shí),由于原來引腳是低電平而導(dǎo)致沒有產(chǎn)生一個(gè)下降沿信號。
(2)IO模擬串口發(fā)送一個(gè)字節(jié)
遵循串口異步傳輸協(xié)議,編寫了STM32上面的相應(yīng)代碼:
#define COM_TX_PORT GPIOA
#define COM_TX_PIN GPIO_Pin_4
#define COM_DATA_HIGH() GPIO_SetBits(COM_TX_PORT, COM_TX_PIN) //高電平
#define COM_DATA_LOW() GPIO_ResetBits(COM_TX_PORT, COM_TX_PIN) //低電平
u32 delayTime;
void VirtualCOM_ByteSend(u8 val)
{
u8 i = 0;
COM_DATA_LOW(); //起始位
Delay_us(delayTime);
for(i = 0; i 《 8; i++) //8位數(shù)據(jù)位
{
if(val & 0x01) COM_DATA_HIGH();
else
COM_DATA_LOW();
Delay_us(delayTime);
val 》》= 1;
}
COM_DATA_HIGH(); //停止位
Delay_us(delayTime);
}
(3)IO模擬串口發(fā)送字符串
既然發(fā)送一個(gè)字節(jié)的函數(shù)已將實(shí)現(xiàn)了,那么發(fā)送字符串函數(shù)就簡單了:
void VirtualCOM_StringSend(u8 *str)
{
while(*str != 0)
{
VirtualCOM_ByteSend(*str);
str++;
}
}
3、IO模擬接收程序
接收的代碼比發(fā)送的代碼復(fù)雜些。先講講怎么IO口接收數(shù)據(jù)的思路。為了接收數(shù)據(jù),IO引腳必須可以檢測到傳輸數(shù)據(jù)的起始位,檢測起始位其實(shí)相當(dāng)于與要檢測一個(gè)下降沿信號,那么引腳只要配置成外部中斷模式就可以檢測到這個(gè)起始信號。然后根據(jù)傳輸速率配置一個(gè)相應(yīng)時(shí)間定時(shí)的定時(shí)器。當(dāng)檢測到起始信號后,打開該定時(shí)器,每隔一定時(shí)間就會進(jìn)入定時(shí)器中斷,檢測當(dāng)前的IO引腳高低電平,從而決定接收到的數(shù)據(jù)是‘1’還是‘0’。當(dāng)?shù)诰糯芜M(jìn)入定時(shí)器中斷服務(wù)程序時(shí),說明已經(jīng)收到了一個(gè)字節(jié)的數(shù)據(jù),此時(shí)關(guān)閉定時(shí)器。
下面在講講怎么在STM32開發(fā)板上實(shí)現(xiàn)這一過程。
(1)選擇IO引腳模擬串口接收引腳RX
我選擇PA5來模擬串口的接收引腳RX,所以需要配置PA5為輸入模式,同時(shí)打開它的外部中斷。
#define COM_RX_PORT GPIOA
#define COM_RX_PIN GPIO_Pin_5v
oid VirtualCOM_RX_GPIOConfig(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStruct;
NVIC_InitTypeDef NVIC_InitStructure;
/* PA5最為數(shù)據(jù)輸入,模擬RX */
GPIO_InitStructure.GPIO_Pin = COM_RX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(COM_RX_PORT, &GPIO_InitStructure);
EXTI_InitStruct.EXTI_Line=EXTI_Line5;
EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Falling;//下降沿都中斷
EXTI_InitStruct.EXTI_LineCmd=ENABLE;
EXTI_Init(&EXTI_InitStruct);
NVIC_InitStructure.NVIC_IRQChannel=EXTI9_5_IRQn; //外部中斷,邊沿觸發(fā)
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
(2)配置一個(gè)定時(shí)器用來定時(shí)接收數(shù)據(jù)
我配置TIM2定時(shí)器為一定的定時(shí)周期,在它的中斷服務(wù)程序中讀取串口發(fā)送過來數(shù)據(jù)。定時(shí)器配置代碼如下:
void TIM2_Configuration(u16 period)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能TIM2的時(shí)鐘
TIM_DeInit(TIM2); //復(fù)位TIM2定時(shí)器
TIM_InternalClockConfig(TIM2); //采用內(nèi)部時(shí)鐘給TIM2提供時(shí)鐘源
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; //預(yù)分頻系數(shù)為72,這樣計(jì)數(shù)器時(shí)鐘為72MHz/72 = 1MHz
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //設(shè)置時(shí)鐘分頻
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//設(shè)置計(jì)數(shù)器模式為向上計(jì)數(shù)模式
TIM_TimeBaseStructure.TIM_Period = period - 1; //設(shè)置計(jì)數(shù)溢出大小,每計(jì)period個(gè)數(shù)就產(chǎn)生一個(gè)更新事件
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure); //將配置應(yīng)用到TIM2中
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除溢出中斷標(biāo)志
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //開啟TIM2的中斷
TIM_Cmd(TIM2,DISABLE); //關(guān)閉定時(shí)器
TIM2 NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //通道設(shè)置為TIM2中斷
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//響應(yīng)式中斷優(yōu)先級0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //打開中斷
NVIC_Init(&NVIC_InitStructure);
}
(3)IO口模擬串口接收功能的實(shí)現(xiàn)
IO口接收串口數(shù)據(jù)的功能是通過PA5引腳的外部中斷服務(wù)程序與定時(shí)器的中斷服務(wù)程序相互配合實(shí)現(xiàn)的。首先需要為數(shù)據(jù)定一些狀態(tài)機(jī),方便標(biāo)識接收到數(shù)據(jù)的狀態(tài):
enum{
COM_START_BIT, //停止位
COM_D0_BIT, //bit0
COM_D1_BIT, //bit1
COM_D2_BIT, //bit2
COM_D3_BIT, //bit3
COM_D4_BIT, //bit4
COM_D5_BIT, //bit5
COM_D6_BIT, //bit6
COM_D7_BIT, //bit7
COM_STOP_BIT, //bit8};
定義好了狀態(tài)機(jī),還需要一個(gè)變量,來保存這些狀態(tài)機(jī)的變化,并定義它的初始狀態(tài)為COM_STOP_BIT:
u8 recvStat = COM_STOP_BIT; //定義狀態(tài)機(jī)
下面是PA5的外部中斷服務(wù)程序,它的主要任務(wù)是檢測起始位,當(dāng)他第一次檢測到下降沿,則說明數(shù)據(jù)即將到來,這時(shí)只要打開定時(shí)器就可以了:
#define COM_RX_STAT GPIO_ReadInputDataBit(COM_RX_PORT, COM_RX_PIN)void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line5)!=RESET)
{
if(!COM_RX_STAT) //檢測引腳高低電平,如果是低電平,則說明檢測到下升沿
{
if(recvStat == COM_STOP_BIT) //狀態(tài)為停止位
{
recvStat = COM_START_BIT; //接收到開始位
Delay(1000); //延時(shí)一定時(shí)間
TIM_Cmd(TIM2, ENABLE); //打開定時(shí)器,接收數(shù)據(jù)
}
}
EXTI_ClearITPendingBit(EXTI_Line5); //清除EXTI_Line1中斷掛起標(biāo)志位
}
}
上面代碼中,檢測到下降沿并設(shè)置了狀態(tài)之后,延時(shí)了一定的時(shí)候,才打開定時(shí)器,這樣做的原因是讓定時(shí)器每次在信號的中間檢測,而不要在信號邊沿檢測。正如下面圖所示:
下面就是定時(shí)器的中斷服務(wù)程序,它主要是接收串口發(fā)送過來的數(shù)據(jù),在它之前我們需要線定義一個(gè)變量用來保存接收到的數(shù)據(jù):
u8 recvData;
然后,定時(shí)器中斷中,每收到1位數(shù)據(jù)就改變下狀態(tài)機(jī)并同時(shí)寫入這個(gè)recvData對應(yīng)的數(shù)據(jù)位中,當(dāng)收到8為數(shù)據(jù)后,然后關(guān)閉定時(shí)器定時(shí),以等待新的數(shù)據(jù)到來:
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) //檢測是否發(fā)生溢出更新事件
{
TIM_ClearITPendingBit(TIM2 , TIM_FLAG_Update);//清除中斷標(biāo)志
recvStat++; //改變狀態(tài)機(jī)
if(recvStat == COM_STOP_BIT) //收到停止位
{
TIM_Cmd(TIM2, DISABLE); //關(guān)閉定時(shí)器 return; //并返回
}
if(COM_RX_STAT) //‘1’
{
recvData |= (1 《《 (recvStat - 1));
}
else //‘0’ { recvData &= ~(1 《《(recvStat - 1));
}
}
}
4、一些精確延時(shí)與不精確延時(shí)的實(shí)現(xiàn)
上面代碼中,需要定義一個(gè)不精確定時(shí)與兩個(gè)精確定時(shí),分別用在檢測到下降沿后延時(shí)一段時(shí)間在打開定時(shí)器和控制傳輸速率中:
void Delay(u32 t){ while(t--);
}
void Delay_us(u32 nus)
{
SysTick-》LOAD=nus*9; //時(shí)間加載
SysTick-》CTRL|=0x01; //開始倒數(shù)
while(!(SysTick-》CTRL&(1《《16)));//等待時(shí)間到達(dá)
SysTick-》CTRL=0X00000000; //關(guān)閉計(jì)數(shù)器
SysTick-》VAL=0X00000000; //清空計(jì)數(shù)器
}
void Delay_ms(u16 nms)
{
SysTick-》LOAD=(u32)nms*9000; //給重裝載寄存器賦值,9000時(shí),將產(chǎn)生1ms的時(shí)基
SysTick-》CTRL|=0x01; //開始倒數(shù) while(!(SysTick-》CTRL&(1《《16))); //等待時(shí)間到達(dá)
SysTick-》CTRL=0X00000000; //關(guān)閉計(jì)數(shù)器 SysTick-》VAL=0X00000000; //清空計(jì)數(shù)器
}
5、編寫一個(gè)配置波特率的函數(shù)
這里能配置的只有300、600、1200三種波特率,其他的波特率我不想弄,也沒有必要弄。下面編寫一個(gè)初始化IO模擬的串口,包括引腳配置、波特率設(shè)置、定時(shí)時(shí)間設(shè)置等:
#define _300BuadRate 3150
#define _600BuadRate 1700
#define _1200BuadRate 800void
VirtualCOM_Config(u16 baudRate)
{
u32 period; VirtualCOM_TX_GPIOConfig();
VirtualCOM_RX_GPIOConfig();
if(baudRate == _300BuadRate) //波特率300 period = _300BuadRate + 250;
else if (baudRate == _600BuadRate) //波特率600 period = _600BuadRate + 50;
else if (baudRate == _1200BuadRate) //波特率1200 period = _1200BuadRate + 50;
TIM2_Configuration(period); //設(shè)置對應(yīng)模特率的定時(shí)器的定時(shí)時(shí)間
delayTime = baudRate; //設(shè)置IO串口發(fā)送的速率
}
要問上面的那些數(shù)字是怎么得來的,實(shí)話說:我是試出來,但是是有根據(jù)地試出來的。我以波特率為1200為例:IO串口發(fā)送函數(shù)VirtualCOM_ByteSend()中,我們用Delay_us(delayTime)來控制傳輸?shù)乃俾剩绻ㄌ芈试O(shè)為1200,則1/1200=830us相當(dāng)于沒830us傳輸1bit數(shù)據(jù),所以在delayTime理論上應(yīng)該設(shè)為830才能保證以波特率1200的速率發(fā)送數(shù)據(jù),但是由于發(fā)送是由代碼實(shí)現(xiàn),有一定的延時(shí),而不像真正串口通過移位寄存器發(fā)送那樣快速,所以需要將delayTime的值在830附近調(diào)整,最后我試出來delayTime=800時(shí),正好實(shí)現(xiàn)了波特率為1200的速率發(fā)送。
同樣的,在IO串口接收時(shí),需要設(shè)定定時(shí)周期,這個(gè)周期也是試出來的,但是也是有依據(jù)的。還是以1200波特率接收為例:理論上應(yīng)該設(shè)置定時(shí)時(shí)間為1/1200=830us,則需要的定時(shí)值為72000000/(1/830us)=72*830,這里設(shè)置定時(shí)器的預(yù)分頻為72,則周期值應(yīng)該為830,所以上面代碼中period的理論上應(yīng)該等于830,但是接收是由代碼寫成的,有一定的延時(shí),而不像真正串口一樣全部有硬件完成那樣快速,所以需要將period的值在830附近調(diào)整,最后試出來period=850時(shí),可以正常接收串口發(fā)送過來的數(shù)據(jù)。
6、其他函數(shù)的編寫
首先需要編寫的BSP_Init()函數(shù),來初始化板子的其他一些外設(shè)的的初始化:
void BSP_Init(void)
{
static volatile ErrorStatus HSEStartUpStatus = SUCCESS;
RCC_DeInit(); //默認(rèn)配置SYSCLK, HCLK, PCLK2, PCLK1, 復(fù)位后就是該配置
RCC_HSEConfig(RCC_HSE_ON); //使能外部高速晶振
HSEStartUpStatus = RCC_WaitForHSEStartUp();//等待外部高速穩(wěn)定
if(HSEStartUpStatus == SUCCESS)
{
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);//使能flash預(yù)讀取緩沖區(qū)
FLASH_SetLatency(FLASH_Latency_2); //令Flash處于等待狀態(tài),2是針對高頻時(shí)鐘的
RCC_HCLKConfig(RCC_SYSCLK_Div1); //HCLK = SYSCLK 設(shè)置高速總線時(shí)鐘=系統(tǒng)時(shí)鐘
RCC_PCLK2Config(RCC_HCLK_Div1); //PCLK2 = HCLK 設(shè)置低速總線2時(shí)鐘=高速總線時(shí)鐘
RCC_PCLK1Config(RCC_HCLK_Div2); //PCLK1 = HCLK/2 設(shè)置低速總線1的時(shí)鐘=高速時(shí)鐘的二分頻
RCC_PLLConfig(RCC_PLLSource_HSE_Div1,
RCC_PLLMul_9); //PLLCLK = 8MHz * 9 = 72 MHz 利用鎖相環(huán)講外部8Mhz晶振9倍頻到72Mhz
RCC_PLLCmd(ENABLE); //使能PLL鎖相環(huán) while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET){} //等待鎖相環(huán)輸出穩(wěn)定 RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);//將鎖相環(huán)輸出設(shè)置為系統(tǒng)時(shí)鐘 while(RCC_GetSYSCLKSource() != 0x08){} //等待校驗(yàn)成功 } //使能GPIO口所使用的時(shí)鐘 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOF|RCC_APB2Periph_GPIOG, ENABLE);
最后是main函數(shù),main函數(shù)很簡單,只要調(diào)用配置IO串口的配置函數(shù)就可以了:
extern u8 recvData;int main(void){ BSP_Init(); VirtualCOM_Config(_600BuadRate); //配置IO模擬串口的波特率為600 VirtualCOM_StringSend(“HelloWorld!\r\n”); //發(fā)送“HelloWorld!”字符串 while(1) { VirtualCOM_ByteSend(recvData); Delay(5000000); }}
7、現(xiàn)象
首先。我們需要用TTL轉(zhuǎn)USB的串口線,連接到電腦,打開串口調(diào)試工具,設(shè)置波特率為600,1位停止位,然后就可以收到IO模擬串口發(fā)過來的“HelloWorld”,然后,我們發(fā)送一個(gè)字符‘a(chǎn)’過去,然后就會每間隔一段時(shí)間打印出該字符。如下圖所示:
評論
查看更多