以NRF52832為例,IO口模擬串列埠
什麼是uart ?
串列埠UART,也稱為通用非同步收發器。採用序列通訊的方式,也就是隻能一位一位的發。串列埠一般需要TX-發射端,RX-接收端,GND-地,三根線,主機的TX 、RX,接從機RX , TX,二者共地。
資料幀的結構
串列埠傳送資料以位元組為單位傳送,傳送的一個位元組,叫一幀資料。
包括,1-bit 開始位,8-bit 資料,1-bit 校驗位(奇/偶校驗)和 1-bit 停止位;
串列埠的TX,RX大部分狀態都是高電平;
開始位是0(低電平),停止位是1(高電平)
8bit資料是按照 先低位後高位的順序傳送。例如傳送96(1001 0110),按照0-1-1-0-1-0-0-1的順序傳送。
校驗位
校驗分為奇校驗和偶校驗;怎麼校驗呢?簡單來說,奇校驗就是檢查資料位中1(高電平)的個數,是否為奇數個,如果不是,校驗位就為1(補1的個數為奇數)。相對的偶校驗就是檢查資料位中1的個數,然後補為偶數個。
例如:資料位是 0X55 , 奇校驗位是1(資料位4個1),如果偶校驗則是0。
波特率
波特率(baud)是指傳送二進位制資料位的速率。常見有115200,9600,4800等。傳送1bit資料的時間 = 1/baud ;
如何用IO口模擬串列埠?
串列埠的作用其實是資料的收和發;其實是可以用IO口模擬的;模擬收資料,其實就是讀電平,即讀到8bit資料的電平,讀1次,延時,再讀,一直讀8次,然後記錄下來,還原成一個位元組。模擬發資料,其實就是拉高拉低電平,模擬真實發資料時TX的電平變化;也是拉電平,延時,再拉。
自寫演算法如下:
模擬傳送:
傳送不用管其他的,只要配置一個io口,根據資料來拉高拉低電平,延時,移位後繼續判斷,再重複進行。最後直接在初始化呼叫發字串的函式,或者放在定時器裡面就可以一直髮。
void TX_IO_INIT(void)
{
nrf_gpio_cfg_output(virtual_tx_io); //輸出
}
void IO_LOW(void)
{
nrf_gpio_pin_write(virtual_tx_io, 0);
}
void IO_HIGH(void)
{
nrf_gpio_pin_write (virtual_tx_io, 1);
}
//模擬串列埠發一個位元組
void virtual_uart_send_byte(uint8 pdata)
{
int i;
int odd = 0;
IO_LOW(); //起始位
nrf_timer_delay_us(208); //波特率4800, t=1000ms/4800 0.2083ms
for(i = 0; i < 8; i++) //8位資料
{
if(pdata & 0x01) //bit 0 高還是低
{
IO_HIGH();
odd++;
}
else
{
IO_LOW();
}
nrf_timer_delay_us(208);
pdata >>= 1; //右移 bit 1, 2,...
}
// 奇校驗位
if(odd%2 == 0)
{
IO_HIGH();
nrf_timer_delay_us(208);
}
else
{
IO_LOW();
nrf_timer_delay_us(208);
}
IO_HIGH(); //停止位,拉高電平
nrf_timer_delay_us(208);
}
//模擬串列埠發一個字串
void virtual_uart_send_string(uint8 *str,uint8 datalen)
{
uint8 i;
for(i = 0; i < datalen; i++)
{
virtual_uart_send_byte(str[i]);
nrf_timer_delay_us(500); //傳送一個位元組延時一下
}
}
模擬接收:
接收是採用外部中斷的形式進入的,當發來一幀資料時,RX口在起始位產生一個下降沿,可以以此標誌進入中斷,中斷掛起,然後讀資料。每次進入中斷,都收到一幀資料。
void virtual_RX_GPIO_init(void)
{
NRF_LOG_INFO("rx_io");
ret_code_t errCode = nrf_drv_gpiote_init(); // GPIOE驅動初始化
APP_ERROR_CHECK(errCode);
nrf_drv_gpiote_in_config_t inConfig = GPIOTE_CONFIG_IN_SENSE_HITOLO(false); //high to low port事件
inConfig.pull = NRF_GPIO_PIN_PULLUP; // 預設上拉
inConfig.sense = GPIOTE_CONFIG_POLARITY_HiToLo; // high to low
errCode = nrf_drv_gpiote_in_init(virtual_rx_io, &inConfig, RX_irqCallbackFunc);
APP_ERROR_CHECK(errCode);
nrf_drv_gpiote_in_event_enable(virtual_rx_io, true);
}
//中斷函式
void RX_irqCallbackFunc(nrf_drv_gpiote_pin_t pin, nrf_gpiote_polarity_t action)
{
if(nrf_gpio_pin_read(virtual_rx_io) == 0) //檢測到低電平 (開始位)
{
s_irqValue = RX_DATA_VALUE;
nrf_drv_gpiote_in_event_disable(virtual_rx_io); //中斷掛起
nrf_timer_delay_us(209);
virtual_RX_HandleIrq(s_irqValue); //延時一個bit後,開始接收8bit資料
nrf_drv_gpiote_in_event_enable(virtual_rx_io, true); //開啟
}
void virtual_RX_HandleIrq(uint8 irqValue)
{
uint8 RX_Data;
uint8 RecvData ;
uint8 i;
if(irqValue & RX_DATA_VALUE)
{
for(i=0;i<8;i++)
{
RX_Data = nrf_gpio_pin_read(virtual_rx_io);
if(RX_Data) // 收到 1
{
RecvData |= 0x80;
}
else //收到 0
{
RecvData &= 0x7F;
}
if(i<7)
{
RecvData>>=1; //移7次
}
nrf_timer_delay_us(209);
}
//校驗位
nrf_timer_delay_us(208);
}
NRF_LOG_INFO("RX %02X",RecvData);
}
關於資料接收準確的問題
前面也說過,影響準確性的最主要原因是時序。如何保證時序正確?最好的是在示波器上看,測試你發的間隔,判讀你讀取的位置,調整延時,使處於最佳位置。
比如,接收資料時,在起始位,你可以多延時半個bit的時間,這樣下次讀取,就在資料位的中間一段,這樣資料會更準確。
你的工程中的其他優先順序較高的程序,可能會響應你的準確性,比如nordic的廣播和掃描。
最後就是你軟體延時的準確性,有可能不準的,同樣會影響你的結果。
自寫的Us延時:
void nrf_timer_delay_us( uint16 number_of_us)
{
NRF_TIMER4->PRESCALER = 4; //2^4 16分頻得到1M timer時鐘
NRF_TIMER4->MODE = 0; //timer模式
NRF_TIMER4->BITMODE = 3; // 設定32bit
NRF_TIMER4->TASKS_CLEAR = 1; //清定時器
NRF_TIMER4->CC[0] = number_of_us; //一個tick是1us,
NVIC_SetPriority(TIMER4_IRQn, 0); //設定中斷優先順序
NVIC_ClearPendingIRQ(TIMER4_IRQn); //清除外部中斷掛起
NRF_TIMER4->TASKS_START = 1; //使能timer模組
while (NRF_TIMER4->EVENTS_COMPARE[0] == 0) //等待計時完成,EVENTS_COMPARE[0] == 1;
{
}
NRF_TIMER4->EVENTS_COMPARE[0] = 0; //清零
NRF_TIMER4->TASKS_STOP = 1; //停止計時
}