第二个教程当然就是串口啦【摸索的时间有点长呢】
串口
LPC54616的串口分为三个部分
- 轮询
- 中断
- DMA
这和STM32差不多,也是三种模式,但是还是内部硬件实现有些差异的
- NXP的串口/SPI/I2S这三种外设是统一叫做Flexcomm 接口来管理,也就是它是那种同一个引脚功能实现了三种功能的玩法
- 这三种外设是共享同一个中断入口的FlexcommX中断,它有很多个Flexcomm中断
- 初始化上需要先初始化Flexcomm接口,然后到具体的串口/SPI/I2S
- 它的串口是内部有FIFO的那种设计,然后功能上没有STM32那么多玩法,后面会讲
让我们开始吧
FLEXCOMM简介
直接点就是这三种串行通信的接口是统一配置和管理的,对指定引脚配置即可切换不同的功能,然后到不同的外设.
基本配置
按这样的顺序
- GPIO时钟与外设初始化
- GPIO初始化
- FLEXCOMM时钟与外设初始化
- 配置FLEXCOMM为串口,配置串口的波特率,起始位,结束位等等基本参数
- 配置串口的中断【可选】
- 配置对应串口的DMA【可选】
以下代码按串口0 P1.5【RX】 P1.6【TX】举例
串口的基本初始化代码
对应步骤1,2,3,4
//设置波特率
static uint8_t USARTx_SetBaudRate(USART_Type *base, uint32_t baudrate_Bps,
uint32_t srcClock_Hz) {
uint32_t best_diff = (uint32_t) -1, best_osrval = 0xf, best_brgval =
(uint32_t) -1;
uint32_t osrval, brgval, diff, baudrate;
/* If synchronous master mode is enabled, only configure the BRG value. */
if ((base->CFG & USART_CFG_SYNCEN_MASK) != 0U) {
if ((base->CFG & USART_CFG_SYNCMST_MASK) != 0U) {
brgval = srcClock_Hz / baudrate_Bps;
base->BRG = brgval - 1U;
}
} else {
/*
* Smaller values of OSR can make the sampling position within a data bit less accurate and may
* potentially cause more noise errors or incorrect data.
*/
for (osrval = best_osrval; osrval >= 8U; osrval--) {
brgval = (((srcClock_Hz * 10U) / ((osrval + 1U) * baudrate_Bps))
- 5U) / 10U;
if (brgval > 0xFFFFU) {
continue;
}
baudrate = srcClock_Hz / ((osrval + 1U) * (brgval + 1U));
diff = baudrate_Bps < baudrate ?
baudrate - baudrate_Bps : baudrate_Bps - baudrate;
if (diff < best_diff) {
best_diff = diff;
best_osrval = osrval;
best_brgval = brgval;
}
}
/* value over range */
if (best_brgval > 0xFFFFU) {
return 1;
}
base->OSR = best_osrval;
base->BRG = best_brgval;
}
return 0;
}
//串口0初始化
void USART0_Base_Init(void) {
/*
GPIO初始化*/
//开启IOCON时钟
//P105
SYSCON->AHBCLKCTRLSET[0] |= 1 << 13;
//P98
//复位外设P100
SYSCON->PRESETCTRLSET[0] |= 1 << 13;
//回读值是不是1
while ((SYSCON->PRESETCTRL[0] & (1 << 13)) == 0) {
;
}
//结束复位外设
SYSCON->PRESETCTRLCLR[0] |= 1 << 13;
//回读值是不是0
while ((SYSCON->PRESETCTRL[0] & (1 << 13)) == (1 << 13)) {
;
}
//使用FLEXCOMM0
//P1_5是RX
//P1_6是TX
//这两个引脚都是Type-D类型的P190
//P194看引脚功能分配
//串口都是
//功能1
//数字模式引脚
//其余默认
IOCON->PIO[1][5] |= (1 << IOCON_PIO_FUNC_SHIFT)
| (1 << IOCON_PIO_DIGIMODE_SHIFT);
IOCON->PIO[1][6] |= (1 << IOCON_PIO_FUNC_SHIFT)
| (1 << IOCON_PIO_DIGIMODE_SHIFT);
//开启FLEXCOMM时钟
SYSCON->AHBCLKCTRLSET[1] |= 1 << 11;
//复位FLEXCOMM外设
SYSCON->PRESETCTRLSET[1] = 1 << 11;
//等待开始复位
while ((SYSCON->PRESETCTRL[1] & (1 << 11)) == 0) {
;
}
//结束复位
//复位FLEXCOMM外设
SYSCON->PRESETCTRLCLR[1] = 1 << 11;
//等待复位结束
while ((SYSCON->PRESETCTRL[1] & (1 << 11)) == (1 << 11)) {
;
}
//配置串口
FLEXCOMM0->PSELID = (uint32_t) 1 << 0;
//配置串口
//FIFO功能
USART0->FIFOCFG |= 1 << USART_FIFOCFG_ENABLETX_SHIFT; //使能发送
USART0->FIFOCFG |= 1 << USART_FIFOCFG_ENABLERX_SHIFT; //使能接收
USART0->FIFOCFG |= 1 << USART_FIFOCFG_EMPTYTX_SHIFT; //清空发FIFO
USART0->FIFOCFG |= 1 << USART_FIFOCFG_EMPTYRX_SHIFT; //清空收FIFO
USART0->FIFOTRIG |= 1 << USART_FIFOTRIG_TXLVLENA_SHIFT; //开启发送FIFO触发
USART0->FIFOTRIG |= 1 << USART_FIFOTRIG_RXLVLENA_SHIFT; //开启接收FIFO触发
USART0->FIFOTRIG &= ~USART_FIFOTRIG_TXLVL_MASK; //发送触发门限为0
USART0->FIFOTRIG |= 0 << USART_FIFOTRIG_TXLVL_SHIFT;
USART0->FIFOTRIG &= ~USART_FIFOTRIG_RXLVL_MASK; //接收触发门限为0
USART0->FIFOTRIG |= 0 << USART_FIFOTRIG_RXLVL_SHIFT;
//波特率,数据长度,奇偶校验等等
USART0->CFG |= 1 << USART_CFG_ENABLE_SHIFT; //EN
USART0->CFG |= 1 << USART_CFG_DATALEN_SHIFT; //8 Bits
USART0->CFG |= 0 << USART_CFG_PARITYSEL_SHIFT; //无校验
USART0->CFG |= 0 << USART_CFG_STOPLEN_SHIFT; //1 Stop
USART0->CFG |= 0 << USART_CFG_CLKPOL_SHIFT;
//波特率配置【我这里试过配置了1M,921600都是能够通信的,串口的时钟是】
uint8_t res = USARTx_SetBaudRate(USART0, 1000000, 12000000);
if (res != 0) {
return;
}
}
串口简介
54616的串口是内部有FIFO的设计,串口和FIFO的配置/状态/中断是分在不同的寄存器中的,使用的时候需要注意
54616的串口的中断功能相比STM32比要弱一些吧
只支持下面这些中断
有发送线空闲,居然没有接收线空闲中断【在STM32上可以做的串口DMA+空闲中断实现不定长数据传输就很难做了】
其他的寄存器就自己看手册吧,这里不过多描述,按流程配置就可以
轮询
基本上和STM32的玩法类似,就是循环填发送寄存器/取接收寄存器的数据,判断发送满/发送线空闲,接收缓冲非空/接收故障
发送
//串口0轮询发送
uint8_t USART0_Tx_Polling(const uint8_t *data, size_t length) {
uint16_t cnt = UARTX_TRY_MAX_CNT;
uint8_t ret = 0;
for (; length > 0U; length--) {
cnt = UARTX_TRY_MAX_CNT;
//缓冲满判断
while ((USART0->FIFOSTAT & USART_FIFOSTAT_TXNOTFULL_MASK) == 0) {
; //如果发送FIFO已经满了
if (cnt > 0) {
cnt--;
} else {
//超时
ret = 0xff;
return ret;
}
}
//开始往FIFO码数据
USART0->FIFOWR = *data;
data++;
cnt = UARTX_TRY_MAX_CNT;
//发送状态空闲判断
while ((USART0->STAT & USART_STAT_TXIDLE_MASK) == USART_STAT_TXIDLE_MASK) {
; //发送忙,等待
if (cnt > 0) {
cnt--;
} else {
//超时
ret = 0xff;
return ret;
}
}
}
return ret;
}
接收
//串口0轮询接收
uint8_t USART0_Rx_Polling(uint8_t *data, size_t length) {
uint8_t ret = 0;
uint32_t statusFlag = 0;
for (; length > 0U; length--) {
//缓冲非空判断
if ((USART0->FIFOSTAT & USART_FIFOSTAT_RXNOTEMPTY_MASK) == 0) {
ret = 0xfe;
break;
}
//故障标志判断
if ((USART0->FIFOSTAT & USART_FIFOSTAT_RXERR_MASK) != 0) {
ret = 0xff;
//清空RX FIFO
USART0->FIFOCFG |= USART_FIFOCFG_EMPTYRX_MASK;
//清除错误标志
USART0->FIFOSTAT |= USART_FIFOSTAT_RXERR_MASK;
break;
}
/* 读状态标志 */
statusFlag = USART0->STAT;
/* 清除所有标志 */
USART0->STAT |= statusFlag;
if ((statusFlag & USART_STAT_PARITYERRINT_MASK) != 0U) {
ret = 0xff;
break;
}
if ((statusFlag & USART_STAT_FRAMERRINT_MASK) != 0U) {
ret = 0xff;
break;
}
if ((statusFlag & USART_STAT_RXNOISEINT_MASK) != 0U) {
ret = 0xff;
break;
}
//开始往FIFO码数据
if (ret == 0) {
*data = (uint8_t) USART0->FIFORD;
data++;
}
}
return ret;
}
这里不过多描述COPY手册的原文,这里寄存器的操作都还不复杂
中断
编写中断函数,编写自己的处理函数【这里只讨论接收中断】
void FLEXCOMM0_IRQHandler(void) {
if ((USART0->FIFOINTSTAT & (1 << USART_FIFOINTSTAT_RXLVL_SHIFT)) != 0) {
//处理接收FIFO水印
//写自己的用户处理函数
//每接收到一个字节就会进来一次
}
if ((USART0->FIFOINTSTAT & (1 << USART_FIFOINTSTAT_RXERR_SHIFT)) != 0) {
//处理接收FIFO错误
//处理接收错误函数
USART0->FIFOSTAT |= 1 << USART_FIFOSTAT_RXERR_SHIFT;
}
}
在串口初始化的末尾加上中断使能的代码
//开启接收FIFO水印中断和接收错误中断
USART0->FIFOINTENSET = USART_FIFOINTENSET_RXLVL_MASK|USART_FIFOINTENSET_RXERR_MASK;
//开中断
NVIC_EnableIRQ(FLEXCOMM0_IRQn);
DMA
先简单介绍下54616的DMA系统
它是只有一个DMA0
按通道区分的,支持优先级,事件触发
它没有具体的模式这种概念,不像STM32那样有【单次,循环,双缓冲】,而是使用一种叫DMA描述符的东西【见下方描述】
描述符就三个信息,源内存的结尾地址,目的内存的结尾地址,下一个描述符的地址【可以为空】
芯片维护了一个DMA通道描述符的表,这个表需要自己定义,并且要512字节对齐
描述符与描述符表格的数据结构如下【参考NXP自己的库函数】
对应STM32的三种模式,NXP这款芯片的DMA的玩法就很灵活了
- 单次传输
- 乒乓传输
- 交替传输
- 链式传输
以上为官方供参考的四种传输模式的实现
应该还可以自己设计出更复杂的传输方式
//描述符结构体的定义
typedef struct _dma_descriptor
{
volatile uint32_t xfercfg; /*!< Transfer configuration */
void *srcEndAddr; /*!< Last source address of DMA transfer */
void *dstEndAddr; /*!< Last destination address of DMA transfer */
void *linkToNextDesc; /*!< Address of next DMA descriptor in chain */
} dma_descriptor_t;
static dma_descriptor_t my_s_dma_descriptor_table0[(30)] __attribute__((aligned((512))));
//按照要求需要定义一个DMA描述符的表,512对齐
static dma_descriptor_t *s_dma_descriptor_table[] =
{ my_s_dma_descriptor_table0 };
DMA的配置流程
- 配置DMA时钟
- 配置DMA外设
- 配置需要的DMA通道
- 开启DMA中断
void MY_DMA0_Init(DMA_Type *base) {
//开启DMA时钟
SYSCON->AHBCLKCTRLSET[0] = 1 << 20;
//开始复位外设
SYSCON->PRESETCTRLSET[0] = 1 << 20;
//等待外设复位开始
while (0u == (SYSCON->PRESETCTRL[0] & (1 << 20))) {
}
//结束复位外设
SYSCON->PRESETCTRLCLR[0] = 1 << 20;
//等待外设复位结束
while ((1 << 20) == (SYSCON->PRESETCTRL[0] & (1 << 20))) {
}
//关联描述符
//P263,注意阅读SRAMBASE的描述
base->SRAMBASE = (uint32_t) s_dma_descriptor_table[0];
//启动DMA外设
base->CTRL |= DMA_CTRL_ENABLE_MASK;
//使能收发DMA通道
//P249
//P264
//FLEXCOM0 RX CH0
base->COMMON[0].ENABLESET |= 1 << 0;
//FLEXCOM0 TX CH1
base->COMMON[0].ENABLESET |= 1 << 1;
//开启DMA全局中断
NVIC_EnableIRQ(DMA0_IRQn);//【可选】
//开启两个通道的中断【可选】
base->COMMON[0].INTENSET |= 1 << 0;
base->COMMON[0].INTENSET |= 1 << 1;
}
DMA收发接口的实现
因为54616没有像STM32那样的空闲中断,它的DMA也没有像STM32那样的DMA传输成功的数据量的寄存器,感觉要实现空闲中断+串口DMA不定长传输会有很大的困难
虽然芯片有接收空闲状态,但是它没有对应的中断,这个就很不方便了
另外它的接收与发送是FIFO模式的,DMA传输成功以后,FIFO的当前数据量都读取完了被清空了,没有办法读取出本次成功传输的数据量来
目前暂时没有太好的办法,下面的代码也只是做到了定长收发
对整个流程不清楚的可以读一读官方写的例子,移植到自己的板子上,看看调用路径,对一对手册,多来几次就可以写出寄存器版本的代码了
DMA发的基本流程
- 开启串口DMA发
- 配置对应通道描述符【内存到外设】
- 判断上一次传输是否完成
- 使能对应通道DMA
- 触发传输
uint8_t UARTx_DMA_TX(USART_Type *base, uint8_t *pTX, uint16_t len) {
uint8_t ret = 0;
//开启串口的DMA发送功能
base->FIFOCFG |= USART_FIFOCFG_DMATX_MASK;
//内存到外设
//获取通道0的描述符地址
dma_descriptor_t *descriptor =
(dma_descriptor_t*) &s_dma_descriptor_table[0][1];
// //判断上一次的传输是否完成
// //P264
if (DMA0->COMMON[0].ACTIVE & (1 << 1)) {
//仍在进行中
ret = 0xff;
}
if (ret == 0) {
//外设请求使能
DMA0->CHANNEL[1].CFG |= DMA_CHANNEL_CFG_PERIPHREQEN_MASK;
//创建描述符
descriptor->srcEndAddr = (uint32_t*) (pTX + (len - 1)); //注意这里是源地址的结束
descriptor->dstEndAddr = (uint32_t*) &base->FIFOWR;
//P271
descriptor->xfercfg |= 1 << 0; //配置有效需要自行开启!!!
//不开重装
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_RELOAD_SHIFT;
//不开软件触发
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_SWTRIG_SHIFT;
//不清触发标志
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_CLRTRIG_SHIFT;
//设置中断A
descriptor->xfercfg |= 1 << DMA_CHANNEL_XFERCFG_SETINTA_SHIFT;
//不设置中断B
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_SETINTB_SHIFT;
//传输宽度8位
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_WIDTH_SHIFT;
//源地址递增1字节
descriptor->xfercfg |= 1 << DMA_CHANNEL_XFERCFG_SRCINC_SHIFT;
//目的地址不递增
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_DSTINC_SHIFT;
//传输数量
descriptor->xfercfg |= (len - 1) << DMA_CHANNEL_XFERCFG_XFERCOUNT_SHIFT;
descriptor->linkToNextDesc = (void*) NULL; //单次传输
DMA0->CHANNEL[1].XFERCFG = descriptor->xfercfg;
//使能DMA通道
DMA0->COMMON[0].ENABLESET |= 1 << 1;
//如果没有开启硬件触发,手动软件触发
if ((DMA0->CHANNEL[1].CFG & DMA_CHANNEL_CFG_HWTRIGEN_MASK) == 0) {
//手动软件触发
DMA0->CHANNEL[1].XFERCFG |= DMA_CHANNEL_XFERCFG_SWTRIG_MASK;
}
}
return ret;
}
DMA收的基本流程
- 开启串口DMA收
- 配置对应通道描述符【外设到内存】
- 判断上一次传输是否完成
- 使能对应通道DMA
- 触发传输
//串口DMA接收
uint8_t UARTx_DMA_RX(USART_Type *base, uint8_t *pRX, uint16_t len) {
uint8_t ret = 0;
//开启串口的DMA接收功能
base->FIFOCFG |= USART_FIFOCFG_DMARX_MASK;
//外设到内存
//获取通道0的描述符地址
dma_descriptor_t *descriptor =
(dma_descriptor_t*) &s_dma_descriptor_table[0][0];
// //判断上一次的传输是否完成
// //P264
if (DMA0->COMMON[0].ACTIVE & (1 << 0)) {
//仍在进行中
ret = 0xff;
}
if (ret == 0) {
//外设请求使能
DMA0->CHANNEL[0].CFG |= DMA_CHANNEL_CFG_PERIPHREQEN_MASK;
//创建描述符
descriptor->srcEndAddr = (uint32_t*) &base->FIFORD;
descriptor->dstEndAddr = (uint32_t*) (pRX + (len - 1)); //注意这里是源地址的结束
//P271
descriptor->xfercfg |= 1 << 0; //配置有效需要自行开启!!!
//不开重装
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_RELOAD_SHIFT;
//不开软件触发
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_SWTRIG_SHIFT;
//不清触发标志
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_CLRTRIG_SHIFT;
//设置中断A
descriptor->xfercfg |= 1 << DMA_CHANNEL_XFERCFG_SETINTA_SHIFT;
//不设置中断B
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_SETINTB_SHIFT;
//传输宽度8位
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_WIDTH_SHIFT;
//源地址不递增
descriptor->xfercfg |= 0 << DMA_CHANNEL_XFERCFG_SRCINC_SHIFT;
//目的地址递增1字节
descriptor->xfercfg |= 1 << DMA_CHANNEL_XFERCFG_DSTINC_SHIFT;
//传输数量
descriptor->xfercfg |= (len - 1) << DMA_CHANNEL_XFERCFG_XFERCOUNT_SHIFT;
descriptor->linkToNextDesc = (void*) NULL; //单次传输
DMA0->CHANNEL[0].XFERCFG = descriptor->xfercfg;
//使能DMA通道
DMA0->COMMON[0].ENABLESET |= 1 << 0;
//如果没有开启硬件触发,手动软件触发
if ((DMA0->CHANNEL[0].CFG & DMA_CHANNEL_CFG_HWTRIGEN_MASK) == 0) {
//手动软件触发
DMA0->CHANNEL[0].XFERCFG |= DMA_CHANNEL_XFERCFG_SWTRIG_MASK;
}
}
return ret;
}
DMA中断
中断里面需要处理传输完成和传输错误
此外他们的每一个DMA通道可以支持配置中断A和中断B,两次触发中断,我们只用A就好
extern uint8_t dma_rx_str[8];//这是我自己定义的接收缓存
//串口0的DMA中断处理函数
static void USART0_DMA_IRQ_Process(void) {
//发送完成处理
//INTA的处理
if ((DMA0->COMMON[0].INTA & (1 << 1)) != 0) {
//清除INTA标志
DMA0->COMMON[0].INTA |= (1 << 1);
//关闭串口DMA发送
USART0->FIFOCFG &= ~(USART_FIFOCFG_DMATX_MASK);
//用户函数可以写在这里
}
//INTB的处理
//INTA的处理
if ((DMA0->COMMON[0].INTB & (1 << 1)) != 0) {
//清除INTA标志
DMA0->COMMON[0].INTB |= (1 << 1);
//关闭串口DMA发送
USART0->FIFOCFG &= ~(USART_FIFOCFG_DMATX_MASK);
//用户函数可以写在这里
}
//接收完成处理
//INTA的处理
if ((DMA0->COMMON[0].INTA & (1 << 0)) != 0) {
//清除INTA标志
DMA0->COMMON[0].INTA |= (1 << 0);
// //关闭串口DMA接收
USART0->FIFOCFG &= ~(USART_FIFOCFG_DMARX_MASK);
//用户函数可以写在这里
}
//INTB的处理
//INTA的处理
if ((DMA0->COMMON[0].INTB & (1 << 0)) != 0) {
//清除INTA标志
DMA0->COMMON[0].INTB |= (1 << 0);
//关闭串口DMA接收
USART0->FIFOCFG &= ~(USART_FIFOCFG_DMARX_MASK);
//用户函数可以写在这里
}
//错误处理
if ((DMA0->COMMON[0].ERRINT & ((1 << 1))) != 0) {
DMA0->COMMON[0].ERRINT |= ((1 << 1));
//用户函数可以写在这里
}
if ((DMA0->COMMON[0].ERRINT & ((1 << 0))) != 0) {
DMA0->COMMON[0].ERRINT |= ((1 << 0));
//用户函数可以写在这里
}
}
void DMA0_IRQHandler(void) {
//串口0
USART0_DMA_IRQ_Process();
}
大概内容就这么多吧
不打算给出完整工程,这种需要自己多实践才行
用这种冷的不行的芯片只能多看看官方例子和啃手册,做实验了
不像STM32的教程烂大街的多
ENJOY~