一、不用串口,打印STM32单片机log
最常用的是通过串口输出uart log,这种方法实现简单,大部分嵌入式芯片都有串口功能。但是这样简单的功能有时候却不是那么好用,比如:
- 一款新拿到的芯片,没有串口驱动时如何打印log
- 某些应用下对时序要求比较高,串口输出log占用时间太长怎么办?比如USB枚举。
- 某些bug正常运行时会出现,当打开串口log时又不再复现怎么办
- 一些封装中没有串口,或者串口已经被用作其他用途,要如何输出log
下文来讨论这些问题。
1 输出log信息到SRAM
准确来说这里并不是输出log,而是以一种方式不使用串口就可以看到log。在芯片开发阶段都可以连接仿真器调试,可以使用打断点的方法调试,但是有些操作如果不能被打断就没法使用断点调试了。
这时候可以考虑将log打印到SRAM中,整个操作结束后再通过仿真器查看SRAM中的log buffer,这样就实现了间接的log输出。
本文使用的测试平台是STM32F407 discovery,基于usb host实验代码,对于其他嵌入式平台原理也是通用的。首先定义一个结构体用于打印log,如下:
定义一段SRAM空间作为log buffer:
static u8 log_buffer[LOG_MAX_LEN];
log buffer是环形缓冲区,在小的buffer就可以无限打印log,缺点也很明显,如果log没有及时输出就会被新的覆盖。Buffer大小根据SRAM大小分配,这里使用1kB。为了方便输出参数,使用printf函数来格式化输出,需要做如下配置(Keil):
并包含头文件#include <stdio.h>, 在代码中实现函数fputc():
写入数据到SRAM:
为了方便控制log打印格式,在头文件中再添加自定义的打印函数。
在需要打印log的地方直接调用DEBUG()即可,最终效果如下,从Memory窗口可以看到打印的log:
2 通过SWO输出log 通过打印log到SRAM的方式可以看到log,但是数据量多的时候可能来不及查看就被覆盖了。为了解决这个问题,可以使用St-link的SWO输出log,这样就不用担心log被覆盖。查看原理图f407 discovery的SWO已经连接了,否则需要自己飞线连接:
在log结构体中添加SWO的操作函数集:
typedef struct
{
u8 (*init)(void* arg);
u8 (*print)(u8 ch);
u8 (*print_dma)(u8* buffer, u32 len);
}log_func;
typedef struct
{
volatile u8 type;
u8* buffer;
volatile u32 write_idx;
volatile u32 read_idx;
//SWO
log_func* swo_log_func;
}log_dev;
SWO只需要print操作函数,实现如下:
u8 swo_print_ch(u8 ch)
{
ITM_SendChar(ch);
return 0;
}
使用SWO输出log同样先输出到log buffer,然后在系统空闲时再输出,当然也可以直接输出。log延迟输出会影响log的实时性,而直接输出会影响到对时间敏感的代码运行,所以如何取舍取决于需要输出log的情形。
在while循环中调用output_ch()函数,就可以实现在系统空闲时输出log。
/*output log buffer to I/O*/
void output_ch(void)
{
u8 ch;
volatile u32 tmp_write,tmp_read;
tmp_write = log_dev_ptr->write_idx;
tmp_read = log_dev_ptr->read_idx;
if(tmp_write != tmp_read)
{
ch = log_dev_ptr->buffer[tmp_read++];
//swo
if(log_dev_ptr->swo_log_func)
log_dev_ptr->swo_log_func->print(ch);
if(tmp_read >= LOG_MAX_LEN)
{
log_dev_ptr->read_idx = 0;
}
else
{
log_dev_ptr->read_idx = tmp_read;
}
}
}
2.1 通过IDE输出
使用IDE中SWO输出功能需要做如下配置(Keil):
在窗口可以看到输出的log:
2.2 通过STM32 ST-LINK Utility输出
使用STM32 ST-LINK Utility不需要做特别的设置,直接打开ST-LINK菜单下的Printf via SWO viewer,然后按start:
3 通过串口输出log
以上都是在串口log暂时无法使用,或者只是临时用一下的方法,而适合长期使用的还是需要通过串口输出log,毕竟大部分时候没法连接仿真器。添加串口输出log只需要添加串口的操作函数集即可:
typedef struct
{
volatile u8 type;
u8* buffer;
volatile u32 write_idx;
volatile u32 read_idx;
volatile u32 dma_read_idx;
//uart
log_func* uart_log_func;
//SWO
log_func* swo_log_func;
}log_dev;
实现串口驱动函数:
添加串口输出log与通过SWO过程类似,不再多叙述。而下面要讨论的问题是,串口的速率较低,输出数据需要较长时间,严重影响系统运行。
虽然可以通过先打印到SRAM再延时输出的办法来减轻影响,但是如果系统中断频繁,或者需要做耗时运算,则可能会丢失log。要解决这个问题,就是要解决CPU与输出数据到串口同时进行的问题,嵌入式工程师立马可以想到DMA正是好的解决途径。
使用DMA搬运log数据到串口输出,同时又不影响CPU运行,这样就可以解决输出串口log耗时影响系统的问题。串口及DMA初始化函数如下:
u8 uart_log_init(void* arg)
{
DMA_InitTypeDef DMA_InitStructure;
u32* bound = (u32*)arg;
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能USART2时钟
//串口2对应引脚复用映射
GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2);
//USART2端口配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOA,&GPIO_InitStructure);
//USART2初始化设置
USART_InitStructure.USART_BaudRate = *bound;//波特率设置
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Tx; //收发模式
USART_Init(USART2, &USART_InitStructure); //初始化串口1
#ifdef LOG_UART_DMA_EN
USART_DMACmd(USART2,USART_DMAReq_Tx,ENABLE);
#endif
USART_Cmd(USART2, ENABLE); //使能串口1
USART_ClearFlag(USART2, USART_FLAG_TC);
while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
#ifdef LOG_UART_DMA_EN
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
//Config DMA channel, uart2 TX usb DMA1 Stream6 Channel
DMA_DeInit(DMA1_Stream6);
DMA_InitStructure.DMA_Channel = DMA_Channel_4;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART2->DR);
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA1_Stream6, &DMA_InitStructure);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
#endif
return 0;
}
DMA输出到串口的函数如下:
这里为了方便直接使用了查询DMA状态寄存器,有需要可以修改为DMA中断方式,查Datasheet可以找到串口2使用DMA1 channel4的stream6:
最后在PC端串口助手可以看到log输出:
使用DMA搬运log buffer中数据到串口,同时CPU可以处理其他事情,这种方式对系统影响最小,并且输出log及时,是实际使用中用的最多的方式。并且不仅可以用串口,其他可以用DMA操作的接口(如SPI、USB)都可以使用这种方法来打印log。
4 使用IO口模拟串口输出log
最后要讨论的是在一些封装中没有串口,或者串口已经被用作其他用途时如何输出log,这时可以找一个空闲的普通IO,模拟UART协议输出log到上位机的串口工具。常用的UART协议如下:
只要在确定的时间在IO上输出高低电平就可以模拟出波形,这个确定的时间就是串口波特率。为了得到精确延时,这里使用TIM4定时器产生1us的延时。注意:定时器不能重复用,在测试工程中TIM2、3都被用了,如果重复用就错乱了。初始化函数如下:
u8 simu_log_init(void* arg)
{
TIM_TimeBaseInitTypeDef TIM_InitStructure;
u32* bound = (u32*)arg;
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_2);
//Config TIM
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE); //使能TIM4时钟
TIM_DeInit(TIM4);
TIM_InitStructure.TIM_Prescaler = 1; //2分频
TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitStructure.TIM_Period = 41; //1us timer
TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM4, &TIM_InitStructure);
TIM_ClearFlag(TIM4, TIM_FLAG_Update);
baud_delay = 1000000/(*bound); //根据波特率计算每个bit延时
return 0;
}
使用定时器的delay函数为:
最后是模拟输出函数,注意:输出前必须要关闭中断,一个byte输出完再打开,否则会出现乱码:
u8 simu_print_ch(u8 ch)
{
volatile u8 i=8;
__asm("cpsid i");
//start bit
GPIO_ResetBits(GPIOA, GPIO_Pin_2);
simu_delay(baud_delay);
while(i--)
{
if(ch & 0x01)
GPIO_SetBits(GPIOA, GPIO_Pin_2);
else
GPIO_ResetBits(GPIOA, GPIO_Pin_2);
ch >>= 1;
simu_delay(baud_delay);
}
//stop bit
GPIO_SetBits(GPIOA, GPIO_Pin_2);
simu_delay(baud_delay);
simu_delay(baud_delay);
__asm("cpsie i");
return 0;
}
使用IO模拟可以达到与真实串口类似的效果,并且只需要一个普通IO,在小封装芯片上比较使用。
二、嵌入式中使用设计模式思想
嵌入式的标签多为:低配,偏硬件,底层,资源紧张,代码多以C语言,汇编为主,代码应用逻辑简单。但随着AIOT时代的到来,局面组件改变。芯片的性能资源逐渐提升,业务逻辑也逐渐变得复杂,相对于代码的效率而言,代码的复用可移植性要求越来越高,以获得更短的项目周期 和更高的可维护性。下面是AIOT时代嵌入式设备的常见的软件框。
设计模式
设计模式的标签:高级语言 ,高端,架构等。在AIOT时代,设计模式与嵌入式能擦出怎样的火花?设计模式可描述为:对于某类相似的问题,经过前人的不断尝试,总结出了处理此类问题的公认的有效解决办法。
嵌入式主要以C语言开发,且面向过程,而设计模式常见于高级语言(面向对象),目前市面上描述设计模式的书籍多数使用JAVA 语言,C语言能实现设计模式吗?设计模式与语言无关,它是解决问题的方法,JAVA可以实现,C语言同样可以实现。同样的,JAVA程序员会遇到需要用模式来处理的问题,C程序员也可能遇见,因此设计模式是很有必要学习的。
模式陷阱:设计模式是针对具体的某些类问题的有效解决办法,不是所有的问题都能匹配到对应的设计模式。因此,不能一味的追求设计模式,有时候简单直接的处理反而更有效。有的问题没有合适的模式,可以尽量满足一些设计原则,如开闭原则(对扩展开放,对修改关闭)
观察者模式
情景
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
实现
主题对象提供统一的注册接口,以及注册函数 。由观察者本身实例化observer_intf 接口,然后使用注册函数,添加到对应的主题列表中,主题状态发生改变,依次通知列表中的所有对象。
struct observer_ops
{
void*(handle)(uint8_t evt);
};
struct observer_intf
{
struct observer_intf* next;
const char* name;
void* condition;
const struct observer_ops *ops;
}
int observer_register(struct topical* top , struct observer_intf* observer);
当主题状态发生改变,将通知到所有观察者,观察者本身也可以设置条件,是否选择接收通知。
struct observer_intf observer_list;
void XXXX_topical_evt(uint8_t evt)
{
struct observer_intf* cur_observer = observer_list.next;
uint8_t* condition = NULL;
while(cur_observer != NULL)
{
condition = (uint8_t*)cur_observer->condition;
if(NULL == condition || (condition && *condition))
{
if(cur_observer->ops->handle){
cur_observer->ops->handle(evt);
}
}
cur_observer = cur_observer->next;
}
}
实例:嵌入式裸机低功耗框架
- 设备功耗分布
其中线路损耗,电源电路等软件无法控制,故不讨论。板载外设,如传感器可能通过某条命令配置进入低功耗模式,又或者硬件上支持控制外设电源来控制功耗。片内外设,及芯片内部的外设,通过卸载相关驱动,关闭时钟配置工作模式来控制功耗。
- 设备唤醒方式
当系统某个定时事件到来时,系统被主动唤醒处理事件
系统处于睡眠,被外部事件唤醒,如串口接收到一包数据,传感器检测到变化,通过引脚通知芯片
- 被动唤醒
- 主动唤醒
- 系统允许睡眠的条件
- 外设无正在收发的数据
- 缓存无需要处理的数据
- 应用层状态处于空闲(无需要处理的事件)
- 基于观察者模式的PM框架实现
PM组件提供的接口:
struct pm
{
struct pm* next;
const char* name;
void(*init)(void);
void(*deinit(void);
void* condition;
};
static struct pm pm_list;
static uint8_t pm_num;
static uint8_t pm_status;
int pm_register(const struct pm* pm , const char* name)
{
struct pm* cur_pm = &pm_list;
while(cur_pm->next)
{
cur_pm = cur_pm->next;
}
cur_pm->next = pm;
pm->next = NULL;
pm->name = name;
pm_num++;
}
void pm_loop(void)
{
uint32_t pm_condition = 0;
struct pm* cur_pm = pm_list.next;
static uint8_t cnt;
/*check all condition*/
while(cur_pm)
{
if(cur_pm->condition){
pm_condition |= *((uint32_t*)(cur_pm->condition));
}
cur_pm = cur_pm->next;
}
if(pm_condition == 0)
{
cnt++;
if(cnt>=5)
{
pm_status = READY_SLEEP;
}
}
else
{
cnt = 0;
}
if( pm_status == READY_SLEEP)
{
cur_pm = pm_list.next;
while(cur_pm)
{
if(cur_pm->deinit){
cur_pm->deinit();
}
cur_pm = cur_pm->next;
}
pm_status = SLEEP;
ENTER_SLEEP_MODE();
}
/*sleep--->wakeup*/
if(pm_status == SLEEP)
{
pm_status = NORMAL;
cur_pm = pm_list.next;
while(cur_pm)
{
if(cur_pm->init){
cur_pm->init();
}
cur_pm = cur_pm->next;
}
}
}
外设使用PM接口:
struct uart_dev
{
...
struct pm pm;
uint32_t pm_condition;
};
struct uart_dev uart1;
void hal_uart1_init(void);
void hal_uart1_deinit(void);
void uart_init(void)
{
uart1.pm.init = hal_uart1_init;
uart1.pm.deinit = hal_uart1_deinit;
uart1.pm.condition = &uart1.pm_condition;
pm_register(&uart1.pm , "uart1");
}
/*接下来串口驱动检查缓存 , 发送 , 接收是否空闲或者忙碌 , 给uart1.pm_condition赋值*/
结论
- PM 电源管理可以单独形成模块,当功耗外设增加时,只需实现接口,注册即可
- 通过定义段导出操作,可以更加简化应用层或外设的注册逻辑
- 方便调试,可以很方便打印出系统当前为满足睡眠条件的模块
- 通过条件字段划分,应该可以实现系统部分睡眠
职责链模式
情景
在现实生活中,一个事件(任务)需要经过多个对象处理是很常见的场景。如报销流程,公司员工报销, 首先员工整理报销单,核对报销金额,有误则继续核对整理,直到无误,将报销单递交到财务,财务部门进行核对,核对无误后,判断金额数量,若小于一定金额,则财务部门可直接审批,若金额超过范围,则报销单流传到总经理,得到批准后,整个任务才算结束。类似的情景还有很多,如配置一个WIFI模块,通过AT指令,要想模块正确连入WIFI , 需要按一定的顺序依次发送配置指令 , 如设置设置模式 ,设置 需要连接的WIFI名,密码,每发送一条配置指令,模块都将返回配置结果,而发送者需要判断结果的正确性,再选择是否发送下一条指令或者进行重传。
总结起来是,一系列任务需要严格按照时间线依次处理的顺序逻辑,如下图所示:
在存在系统的情况下,此类逻辑可以很容易的用阻塞延时来实现,实现如下:
void process_task(void)
{
task1_process();
msleep(1000);
task2_process();
mq_recv(¶m , 1000);
task3_process();
while(mq_recv(¶m , 1000) != OK)
{
if(retry)
{
task3_process();
--try;
}
}
}
在裸机的情况下,为了保证系统的实时性,无法使用阻塞延时,一般使用定时事件配合状态机来实现:
void process_task(void)
{
switch(task_state)
{
case task1:
task1_process();
set_timeout(1000);break;
case task2:
task1_process();
set_timeout(1000);break;
case task3:
task1_process();
set_timeout(1000)break;
default:break;
}
}
/*定时器超时回调*/
void timeout_cb(void)
{
if(task_state == task1)
{
task_state = task2;
process_task();
}
else //task2 and task3
{
if(retry)
{
retry--;
process_task();
}
}
}
/*任务的应答回调*/
void task_ans_cb(void* param)
{
if(task==task2)
{
task_state = task3;
process_task();
}
}
和系统实现相比,裸机的实现更加复杂,为了避免阻塞,只能通过状态和定时器来实现顺序延时的逻辑,可以看到,实现过程相当分散,对于单个任务的处理分散到了3个函数中处理,这样导致的后果是:修改,移植的不便。而实际的应用中,类似的逻辑相当多,如果按照上面的方法去实现,将会导致应用程序的强耦合。
实现
可以发现,上面的情景有以下特点:
- 任务按顺序执行,只有当前任务执行完了(有结论,成功或者失败)才允许执行下一个任务
- 前一个任务的执行结果会影响到下一个任务的执行情况
- 任务有一些特性,如超时时间,延时时间,重试次数
通过以上信息,我们可以抽象出这样一个模型:任务作为节点, 每一个任务节点有其属性:如超时,延时,重试,参数,处理方法,执行结果。当需要按照顺序执行一系列任务时,依次将任务节点串成一条链,启动链运行,则从任务链的第一个节点开始运行,运行的结果可以是 OK , BUSY ,ERROR 。若是OK, 表示节点已处理,从任务链中删除,ERROR 表示运行出错,任务链将停止运行,进行错误回调,可以有用户决定是否继续运行下去。BUSY表示任务链处于等待应答,或者等待延时的情况。当整条任务链上的节点都执行完,进行成功回调。
node数据结构定义:
/*shadow node api type for req_chain src*/
typedef struct shadow_resp_chain_node
{
uint16_t timeout;
uint16_t duration;
uint8_t init_retry;
uint8_t param_type;
uint16_t retry;
/*used in mpool*/
struct shadow_resp_chain_node* mp_prev;
struct shadow_resp_chain_node* mp_next;
/*used resp_chain*/
struct shadow_resp_chain_node* next;
node_resp_handle_fp handle;
void* param;
}shadow_resp_chain_node_t;
node内存池:
使用内存池的必要性:实际情况下,同一时间,责任链的条数,以及单条链的节点数比较有限,但种类是相当多的。比如一个支持AT指令的模块,可能支持几十条AT指令,但执行一个配置操作,可能就只会使用3-5条指令,若全部静态定义节点,将会消耗大量内存资源。因此动态分配是必要的。
初始化node内存池,内存池内所有节点都将添加到free_list。当申请节点时,会取出第一个空闲节点,加入到used_list , 并且接入到责任链。当责任链某一个节点执行完,将会被自动回收(从责任链中删除,并从used_list中删除,然后添加到free_list)
职责链数据结构定义:
typedef struct resp_chain
{
bool enable; //enble == true 责任链启动
bool is_ans; //收到应答,与void* param 共同组成应答信号
uint8_t state;
const char* name;
void* param;
TimerEvent_t timer;
bool timer_is_running;
shadow_resp_chain_node_t node; //节点链
void(*resp_done)(void* result); //执行结果回调
}resp_chain_t;
职责链初始化:
void resp_chain_init(resp_chain_t* chain , const char* name ,
void(*callback)(void* result))
{
RESP_ASSERT(chain);
/*only init one time*/
resp_chain_mpool_init();
chain->enable = false;
chain->is_ans = false;
chain->resp_done = callback;
chain->name = name;
chain->state = RESP_STATUS_IDLE;
chain->node.next = NULL;
chain->param = NULL;
TimerInit(&chain->timer,NULL);
}
职责链添加节点:
int resp_chain_node_add(resp_chain_t* chain ,
node_resp_handle_fp handle , void* param)
{
RESP_ASSERT(chain);
BoardDisableIrq();
shadow_resp_chain_node_t* node = chain_node_malloc();
if(node == NULL)
{
BoardEnableIrq();
RESP_LOG("node malloc error ,no free node");
return -2;
}
/*初始化节点,并加入责任链*/
shadow_resp_chain_node_t* l = &chain->node;
while(l->next != NULL)
{
l = l->next;
}
l->next = node;
node->next = NULL;
node->handle = handle;
node->param = param;
node->timeout = RESP_CHIAN_NODE_DEFAULT_TIMEOUT;
node->duration = RESP_CHIAN_NODE_DEFAULT_DURATION;
node->init_retry = RESP_CHIAN_NODE_DEFAULT_RETRY;
node->retry = (node->init_retry == 0)? 0 :(node->init_retry-1);
BoardEnableIrq();
return 0;
}
职责链的启动:
void resp_chain_start(resp_chain_t* chain)
{
RESP_ASSERT(chain);
chain->enable = true;
}
职责链的应答:
void resp_chain_set_ans(resp_chain_t* chain , void* param)
{
RESP_ASSERT(chain);
if(chain->enable)
{
chain->is_ans = true;
if(param != NULL)
chain->param = param;
else
{
chain->param = "NO PARAM";
}
}
}
职责链的运行:
int resp_chain_run(resp_chain_t* chain)
{
RESP_ASSERT(chain);
if(chain->enable)
{
shadow_resp_chain_node_t* cur_node = chain->node.next;
/*maybe ans occur in handle,so cannot change state direct when ans comming*/
if(chain->is_ans)
{
chain->is_ans = false;
chain->state = RESP_STATUS_ANS;
}
switch(chain->state)
{
case RESP_STATUS_IDLE:
{
if(cur_node)
{
uint16_t retry = cur_node->init_retry;
if(cur_node->handle)
{
cur_node->param_type = RESP_PARAM_INPUT;
chain->state = cur_node->handle((resp_chain_node_t*)cur_node ,cur_node->param);
}
else
{
RESP_LOG("node handle is null ,goto next node");
chain->state = RESP_STATUS_OK;
}
if(retry != cur_node->init_retry)
{
cur_node->retry = cur_node->init_retry>0?(cur_node- >init_retry-1):0;
}
}
else
{
if(chain->resp_done)
{
chain->resp_done((void*)RESP_RESULT_OK);
}
chain->enable = 0;
chain->state = RESP_STATUS_IDLE;
TimerStop(&chain->timer);
chain->timer_is_running = false;
}
break;
}
case RESP_STATUS_DELAY:
{
if(chain->timer_is_running == false)
{
chain->timer_is_running = true;
TimerSetValueStart(&chain->timer , cur_node->duration);
}
if(TimerGetFlag(&chain->timer) == true)
{
chain->state = RESP_STATUS_OK;
chain->timer_is_running = false;
}
break;
}
case RESP_STATUS_BUSY:
{
/*waiting for ans or timeout*/
if(chain->timer_is_running == false)
{
chain->timer_is_running = true;
TimerSetValueStart(&chain->timer , cur_node->timeout);
}
if(TimerGetFlag(&chain->timer) == true)
{
chain->state = RESP_STATUS_TIMEOUT;
chain->timer_is_running = false;
}
break;
}
case RESP_STATUS_ANS:
{
/*already got the ans,put the param back to the request handle*/
TimerStop(&chain->timer);
chain->timer_is_running = false;
if(cur_node->handle)
{
cur_node->param_type = RESP_PARAM_ANS;
chain->state = cur_node->handle((resp_chain_node_t*)cur_node , chain->param);
}
else
{
RESP_LOG("node handle is null ,goto next node");
chain->state = RESP_STATUS_OK;
}
break;
}
case RESP_STATUS_TIMEOUT:
{
if(cur_node->retry)
{
cur_node->retry--;
/*retry to request until cnt is 0*/
chain->state = RESP_STATUS_IDLE;
}
else
{
chain->state = RESP_STATUS_ERROR;
}
break;
}
case RESP_STATUS_ERROR:
{
if(chain->resp_done)
{
chain->resp_done((void*)RESP_RESULT_ERROR);
}
chain->enable = 0;
chain->state = RESP_STATUS_IDLE;
TimerStop(&chain->timer);
chain->timer_is_running = false;
cur_node->retry = cur_node->init_retry>0?(cur_node->init_retry-1):0;
chain_node_free_all(chain);
break;
}
case RESP_STATUS_OK:
{
/*get the next node*/
cur_node->retry = cur_node->init_retry>0?(cur_node->init_retry-1):0;
chain_node_free(cur_node);
chain->node.next = chain->node.next->next;
chain->state = RESP_STATUS_IDLE;
break;
}
default:
break;
}
}
return chain->enable;
}
测试用例:
- 定义并初始化责任链
void chain_test_init(void)
{
resp_chain_init(&test_req_chain , "test request" , test_req_callback);
}
设计模式的C语言应用.https://bbs.huaweicloud.com/blogs/113179
适配器模式
设计模式参考
Head First 设计模式(中文版)
人人都懂设计模式:从生活中领悟设计模式:Python实现
设计模式之禅
- 定义运行函数,在主循环中调用
void chain_test_run(void)
{
resp_chain_run(&test_req_chain);
}
- 测试节点添加并启动触发函数
void chain_test_tigger(void)
{
resp_chain_node_add(&test_req_chain , node1_req ,NULL);
resp_chain_node_add(&test_req_chain , node2_req,NULL);
resp_chain_node_add(&test_req_chain , node3_req,NULL);
resp_chain_start(&test_req_chain);
}
- 分别实现节点请求函数
/*延时1s 后执行下一个节点*/
int node1_req(resp_chain_node_t* cfg, void* param)
{
cfg->duration = 1000;
RESP_LOG("node1 send direct request: delay :%d ms" , cfg->duration);
return RESP_STATUS_DELAY;
}
/*超时时间1S , 重传次数5次*/
int node2_req(resp_chain_node_t* cfg , void* param)
{
static uint8_t cnt;
if(param == NULL)
{
cfg->init_retry = 5;
cfg->timeout = 1000;
RESP_LOG("node2 send request max retry:%d , waiting for ans...");
return RESP_STATUS_BUSY;
}
RESP_LOG("node2 get ans: %d",(int)param);
return RESP_STATUS_OK;
}
/*非异步请求*/
int node3_req(resp_chain_node_t* cfg , void* param)
{
RESP_LOG("node4 send direct request");
return RESP_STATUS_OK;
}
void ans_callback(void* param)
{
resp_chain_set_ans(&test_req_chain , param);
}
结论
- 实现了裸机处理 顺序延时任务
- 较大程度的简化了应用程的实现,用户只需要实现响应的处理函数 , 调用接口添加,即可按时间要求执行
- 参数为空,表明为请求 ,否则为应答。(在某些场合,请求可能也带参数,如接下来所说的LAP协议,此时需要通过判断参数的类型)
三、解读射频芯片
传统来说,一部可支持打电话、发短信、网络服务、APP应用的手机,一般包含五个部分部分:射频部分、基带部分、电源管理、外设、软件。
- 射频部分:一般是信息发送和接收的部分;
- 基带部分:一般是信息处理的部分;
- 电源管理:一般是节电的部分,由于手机是能源有限的设备,所以电源管理十分重要;
- 外设:一般包括LCD,键盘,机壳等;
- 软件:一般包括系统、驱动、中间件、应用。
在手机终端中,最重要的核心就是射频芯片和基带芯片。射频芯片负责射频收发、频率合成、功率放大;基带芯片负责信号处理和协议处理。那么射频芯片和基带芯片是什么关系?
射频芯片和基带芯片的关系
先讲一下历史,射频(Radio Frenquency)和基带(Base Band)皆来自英文直译。其中射频最早的应用就是Radio——无线广播(FM/AM),迄今为止这仍是射频技术乃至无线电领域最经典的应用。
基带则是band中心点在0Hz的信号,所以基带就是最基础的信号。有人也把基带叫做“未调制信号”,曾经这个概念是对的。例如AM为调制信号,无需调制,接收后即可通过发声元器件读取内容。
但对于现代通信领域而言,基带信号通常都是指经过数字调制的,频谱中心点在0Hz的信号。而且没有明确的概念表明基带必须是模拟或者数字的,这完全看具体的实现机制。
言归正传,基带芯片可以认为是包括调制解调器,但不止于调制解调器,还包括信道编解码、信源编解码,以及一些信令处理。而射频芯片,则可看做是最简单的基带调制信号的上变频和下变频。
所谓调制,就是把需要传输的信号,通过一定的规则调制到载波上面让后通过无线收发器(RF Transceiver)发送出去的工程,解调就是相反的过程。
工作原理与电路分析
射频简称RF射频就是射频电流,是一种高频交流变化电磁波,为是Radio Frequency的缩写,表示可以辐射到空间的电磁频率,频率范围在300KHz~300GHz之间。每秒变化小于1000次的交流电称为低频电流,大于10000次的称为高频电流,而射频就是这样一种高频电流。高频(大于10K);射频(300K-300G)是高频的较高频段;微波频段(300M-300G)又是射频的较高频段。射频技术在无线通信领域中被广泛使用,有线电视系统就是采用射频传输方式。
射频芯片指的就是将无线电信号通信转换成一定的无线电信号波形, 并通过天线谐振发送出去的一个电子元器件,它包括功率放大器、低噪声放大器和天线开关。
射频芯片架构包括接收通道和发射通道两大部分,射频电路方框图如下。
接收电路的结构和工作原理
接收时,天线把基站发送来电磁波转为微弱交流电流信号经滤波,高频放大后,送入中频内进行解调,得到接收基带信息(RXI-P、RXI-N、RXQ-P、RXQ-N);送到逻辑音频电路进一步处理。
该电路掌握重点:
- 接收电路结构;
- 各元件的功能与作用;
- 接收信号流程。
电路结构
接收电路由天线、天线开关、滤波器、高放管(低噪声放大器)、中频集成块(接收解调器)等电路组成。早期手机有一级、二级混频电路,其目的把接收频率降低后再解调,接收电路方框图如下。
各元件的功能与作用
1)、手机天线:
结构如下图,由手机天线分外置和内置天线两种;由天线座、螺线管、塑料封套组成。
作用:
- 接收时把基站发送来电磁波转为微弱交流电流信号。
- 发射时把功放放大后的交流电流转化为电磁波信号。
2)、天线开关:
结构如下图,手机天线开关(合路器、双工滤波器)由四个电子开关构成。
作用:
- 完成接收和发射切换;
- 完成900M/1800M信号接收切换。
逻辑电路根据手机工作状态分别送出控制信号(GSM-RX-EN;DCS- RX-EN;GSM-TX-EN;DCS- TX-EN),令各自通路导通,使接收和发射信号各走其道,互不干扰。
由于手机工作时接收和发射不能同时在一个时隙工作(即接收时不发射,发射时不接收)。因此后期新型手机把接收通路的两开关去掉,只留两个发射转换开关;接收切换任务交由高放管完成。
3)、滤波器:
手机中有高频滤波器、中频滤波器。
作用:滤除其他无用信号,得到纯正接收信号。后期新型手机都为零中频手机;因此,手机中再没有中频滤波器。
4)、高放管(高频放大管、低噪声放大器):
手机中高放管有两个:900M高放管、1800M高放管。都是三极管共发射极放大电路;后期新型手机把高放管集成在中频内部。高频放大管供电图如下。
作用:
- 对天线感应到微弱电流进行放大,满足后级电路对信号幅度的需求。
- 完成900M/1800M接收信号切换。
原理:
a)、供电:900M/1800M两个高放管的基极偏压共用一路,由中频同时路提供;而两管的集电极的偏压由中频CPU根据手机的接收状态命令中频分两路送出;其目的完成900M/1800M接收信号切换。
b)、经过滤波器滤除其他杂波得到纯正935M-960M的接收信号由电容器耦合后送入相应的高放管放大后经电容器耦合送入中频进行后一级处理。
5)、中频(射频接囗、射频信号处理器):
结构:由接收解调器、发射调制器、发射鉴相器等电路组成;新型手机还把高放管、频率合成、26M振荡及分频电路也集成在内部,如下图。
作用:
- 内部高放管把天线感应到微弱电流进行放大;
- 接收时把935M-960M(GSM)的接收载频信号(带对方信息)与本振信号(不带信息)进行解调,得到67.707KHZ的接收基带信息;
- 发射时把逻辑电路处理过的发射信息与本振信号调制成发射中频;
- 结合13M/26M晶体产生13M时钟(参考时钟电路);
- 根据CPU送来参考信号,产生符合手机工作信道的本振信号。
接收信号流程
手机接收时,天线把基站发送来电磁波转为微弱交流电流信号,经过天线开关接收通路,送高频滤波器滤除其它无用杂波,得到纯正935M-960M(GSM)的接收信号,由电容器耦合送入中频内部相应的高放管放大后,送入解调器与本振信号(不带信息)进行解调,得到67.707KHZ的接收基带信息(RXI-P、RXI-N、RXQ-P、RXQ-N);送到逻辑音频电路进一步处理。
发射电路的结构和工作原理
发射时,把逻辑电路处理过的发射基带信息调制成的发射中频,用TX-VCO把发射中频信号频率上变为890M-915M(GSM)的频率信号。经功放放大后由天线转为电磁波辐射出去。
该电路掌握重点:
- 电路结构;
- 各元件的功能与作用;
- 发射信号流程。
电路结构
发射电路由中频内部的发射调制器、发射鉴相器;发射压控振荡器(TX-VCO)、功率放大器(功放)、功率控制器(功控)、发射互感器等电路组成,发射电路方框图如下。
各元件的功能与作用
1)、发射调制器:
发射调制器在中频内部,相当于宽带网络中的MOD。
作用:发射时把逻辑电路处理过的发射基带信息(TXI-P;TXI-N;TXQ-P;TXQ-N)与本振信号调制成发射中频。
2)、发射压控振荡器(TX-VCO):
发射压控振荡器是由电压控制输出频率的电容三点式振荡电路;在生产制造时集成为一小电路板上,引出五个脚:供电脚、接地脚、输出脚、控制脚、900M/1800M频段切换脚。当有合适工作电压后便振荡产生相应频率信号。
作用:把中频内调制器调制成的发射中频信号转为基站能接收的890M-915M(GSM)的频率信号。
众所周知,基站只能接收890M-915M(GSM)的频率信号,而中频调制器调制的中频信号(如三星发射中频信号135M)基站不能接收的,因此,要用TX-VCO把发射中频信号频率上变为890M-915M(GSM)的频率信号。
当发射时,电源部分送出3VTX电压使TX-VCO工作,产生890M-915M(GSM)的频率信号分两路走:a)、取样送回中频内部,与本振信号混频产生一个与发射中频相等的发射鉴频信号,送入鉴相器中与发射中频进行较;若TX-VCO振荡出频率不符合手机的工作信道,则鉴相器会产生1-4V跳变电压(带有交流发射信息的直流电压)去控制TX-VCO内部变容二极管的电容量,达到调整频率准确性目的。b)、送入功放经放大后由天线转为电磁波辐射出去。
从上看出:由TX-VCO产生频率到取样送回中频内部,再产生电压去控制TX-VCO工作;刚好形成一个闭合环路,且是控制频率相位的,因此该电路也称发射锁相环电路。
3)、功率放大器(功放):
目前手机的功放为双频功放(900M功放和1800M功放集成一体),分黑胶功放和铁壳功放两种;不同型号功放不能互换。
作用:把TX-VCO振荡出频率信号放大,获得足够功率电流,经天线转化为电磁波辐射出去。
值得注意的是:功放放大的是发射频率信号的幅值,不能放大他的频率。
功率放大器的工作条件:
- 工作电压(VCC):手机功放供电由电池直接提供(3.6V);
- 接地端(GND):使电流形成回路;
- 双频功换信号(BANDSEL):控制功放工作于900M或工作于1800M;
- 功率控制信号(PAC):控制功放的放大量(工作电流);
- 输入信号(IN);输出信号(OUT)。
4)、发射互感器:
两个线径和匝数相等的线圈相互靠近,利用互感原理组成。
作用:把功放发射功率电流取样送入功控。
当发射时功放发射功率电流经过发射互感器时,在其次级感生与功率电流同样大小的电流,经检波(高频整流)后并送入功控。
5)、功率等级信号:
所谓功率等级就是工程师们在手机编程时把接收信号分为八个等级,每个接收等级对应一级发射功率(如下表),手机在工作时,CPU根据接的信号强度来判断手机与基站距离远近,送出适当的发射等级信号,从而来决定功放的放大量。即接收强时,发射就弱。附功率等级表:
6)、功率控制器(功控):
结构为一个运算比较放大器。
作用:把发射功率电流取样信号和功率等级信号进行比较,得到一个合适电压信号去控制功放的放大量。
当发射时功率电流经过发射互感器时,在其次级感生的电流,经检波(高频整流)后并送入功控;同时编程时预设功率等级信号也送入功控;两个信号在内部比较后产生一个电压信号去控制功放的放大量,使功放工作电流适中,既省电又能长功放使用寿命。因为功控电压高,功放功率就大。
发射信号流程
当发射时,逻辑电路处理过的发射基带信息(TXI-P;TXI-N;TXQ-P;TXQ-N),送入中频内部的发射调制器,与本振信号调制成发射中频。而中频信号基站不能接收的,要用TX-VCO把发射中频信号频率上升为890M-915M(GSM)的频率信号基站才能接收。当TX-VCO工作后,产生890M-915M(GSM)的频率信号分两路走:
a)、一路取样送回中频内部,与本振信号混频产生一个与发射中频相等的发射鉴频信号,送入鉴相器中与发射中频进行较;若TX-VCO振荡出频率不符合手机的工作信道,则鉴相器会产生一个1-4V跳变电压去控制TX-VCO内部变容二极管的电容量,达到调整频率目的。
b)、二路送入功放经放大后由天线转化为电磁波辐射出去。为了控制功放放大量,当发射时功率电流经过发射互感器时,在其次级感生的电流,经检波(高频整流)后并送入功控;同时编程时预设功率等级信号也送入功控;两个信号在内部比较后产生一个电压信号去控制功放的放大量,使功放工作电流适中,既省电又能长功放使用寿命。
国产射频芯片产业链现状
在射频芯片领域,市场主要被海外巨头所垄断,海外的主要公司有Qrovo,skyworks和Broadcom;国内射频芯片方面,没有公司能够独立支撑IDM的运营模式,主要为Fabless设计类公司;国内企业通过设计、代工、封装环节的协同,形成了“软IDM“”的运营模式。
射频芯片设计方面,国内公司在5G芯片已经有所成绩,具有一定的出货能力。射频芯片设计具有较高的门槛,具备射频开发经验后,可以加速后续高级品类射频芯片的开发。目前,具备射频芯片设计的公司有紫光展锐、唯捷创芯、中普微、中兴通讯、雷柏科技、华虹设计、江苏钜芯、爱斯泰克等。
射频芯片代工方面,台湾已经成为全球最大的化合物半导体芯片代工厂,台湾主要的代工厂有稳懋、宏捷科和寰宇,国内仅有三安光电和海威华芯开始涉足化合物半导体代工。三安光电是国内目前国内布局最为完善,具有GaAs HBT/pHEMT和 GaNSBD/FET 工艺布局,目前在于国内200多家企事业单位进行合作,有10多种芯片通过性能验证,即将量产。海威华芯为海特高新控股的子公司,与中国电科29所合资,目前具有GaAs 0.25um PHEMT工艺制程能力。
射频芯片封装方面,5G射频芯片一方面频率升高导致电路中连接线的对电路性能影响更大,封装时需要减小信号连接线的长度;另一方面需要把功率放大器、低噪声放大器、开关和滤波器封装成为一个模块,一方面减小体积另一方面方便下游终端厂商使用。为了减小射频参数的寄生需要采用Flip-Chip、Fan-In和Fan-Out封装技术。
Flip-Chip和Fan-In、Fan-Out工艺封装时,不需要通过金丝键合线进行信号连接,减少了由于金丝键合线带来的寄生电效应,提高芯片射频性能;到5G时代,高性能的Flip-Chip/Fan-In/Fan-Out结合Sip封装技术会是未来封装的趋势。Flip-Chip/Fan-In/Fan-Out和Sip封装属于高级封装,其盈利能力远高于传统封装。
四、keil中的代码格式化插件——Astyle
在keil中一行一行的缩进自己的代码,虽然注重代码的格式是一个好的习惯,不过这样代码比较多的话,整理起来效率太低,推荐Astyle这个工具,同时这里网上也找了点内容分享给大家:
1 Astyle介绍
Astyle全称Artistic Style,是一个免费,快速,小型的自动格式化程序,适用于C,C++,C++/CLI,Objective‑C,C#和Java源代码。
在缩进源代码时,作为程序员,我们倾向于同时使用空格和制表符来创建所需的缩进。此外,默认情况下,某些编辑器在按 Tab 键时会插入空格而不是制表符。其他编辑器(例如Emacs)能够通过自动设置行上代码之前的空格来"漂亮地"行,可能在代码中插入空格,到目前为止,这些代码仅使用制表符进行缩进。
源代码中每个制表符的空格数可以在编辑器之间更改(除非用户根据自己的喜好设置数字...)。程序员在从一个编辑器移动到另一个编辑器时面临的一个标准问题是,包含空格和制表符的代码(完全缩进)突然变得一团糟。即使您作为程序员只注意使用空格或制表符,查看其他人的源代码仍然会有问题。
为了解决这个问题,创建了Art Style——一个用C++编写的过滤器,可以自动重新缩进和重新格式化C / C++ / Objective-C / C++ / CLI / C# / Java源文件。它可以从命令行使用,也可以作为库合并到另一个程序中。
官网地址:http://astyle.sourceforge.net/
下载地址: https://sourceforge.net/projects/astyle/
2 Astyle初体验
直观一点,我们来看看下面没有经过Astyle格式化的代码:
接下来我们用Astyle插件给它格式化看一下:
很明显,第一种代码风格要是每次参考别人的代码看到的是这样的,那基本连继续看下去的心情都没有了,果断右键回收站,第二种代码风格,看起来就舒服多了。
也就是说,以后但凡看别人的代码,第一件事先用Astyle插件把代码风格格式化一下,变成合适自己的那种风格,也有利于提高自己的学习和工作效率;一个好的程序,不仅要有好的算法,同时也需要有良好的书写风格。
3 Astyle插件在Keil工程中的适配
这里我使用的的Keil软件版本是:V5.29.0.0
3.1 keil5配置用户自定义插件
选择Customize Tools Menu。3.2 添加用户自定义命令并配置参数
其中Menu Content就是自定义的用户命令,名称可以自己写,接下来看看我做的两个用户命令。
用户命令:格式化工程
参数配置:(这个是自己参考别人的方法配置的)
-n "$E*.c" "$E*.h" --style=ansi -p -s4 -S -f -xW -w -xw -R
用户命令:格式化当前文件
参数配置:(这个是自己参考别人的方法配置的)
-n !E --style=ansi -p -s4 -S -f -xW -w -xw
注:!E表示的是当前获得焦点且正在编辑的文件。 $E*.c和$E*.h代表当前获得焦点且正在编辑文件所在目录下所有.c和.h文件(参考keil uVision的帮助文档)使用的是Astyle默认格式来格式化文件,另外也可以自定义格式,自定义格式参考Astyle官网的帮助文档。
3.3 选择命令对.c 或 .h开始格式化处理
然后点击自己配置的用户命令即可完成代码格式化,其它的风格详情查看官网的开发文档学习。