程序设计的一个重要方面,是处理过程划分。
这是结构化的基本要求。
将处理过程划分为一个个片段,这就是结构化之后形成的函数。
函数的基本特征就是,接受一定的输入数据,经过处理后,产生一定的输出数据。
这其中的核心,就是函数间通信。
函数间通信,发生在调用者和被调用者之间,也发生在上游函数调用和下游函数调用之间,也发生在前台ISR函数调用和后台函数调用之间,不一而足。
每一次函数调用,会拥有自己的一个局部变量区域,位于stack之中,通常称为stack frame。
局部变量,包括了入口参数,return,内部定义的局部变量。
调用者和被调用者通信,就需要填充被调用者的stack frame中的entry param区域,
另一方面,被调用者的return variable,则被复制到调用者的局部变量中,或者由pointer所指定的位置。
可以称之为variable transfering。
除此之外,其他形式的函数间通信,都需要用到一种通信方式,即variable sharing。
而variable sharing,则是本文讨论的重点。
在SOD或者OOD设计中,
variable sharing是推荐的设计思想。
设计static变量,是一种variable sharing。
但是一旦数量庞大,则管理起来将是一件困难的事情。
为此,我们需要设计struct,将相关的variable封装起来,便于管理。
当系统内存在大量的struct后,管理起来又是一件困难的事情,
为此,我们需要设计pointer,将相关的struct关联起来,便于索引需要的数据。
这就是SOD的基本设计思想。
随着函数数量的增加,如果仅仅使用SOD设计,则函数管理起来,仍然是一件困难的事情。
为此,我们需要设计class,一方面,将相关的variable封装起来,一方面,将相关的函数也封装起来。当发生函数调用时,系统能够找到正确的代码段。
总之,主要以variable sharing为主的函数间通信方式,
成为SOD设计或者OOD设计的必然选择。
同一个class中定义的成员函数,它们之间的通信方式,推荐是variable sharing方式。
不同的class中定义的成员函数,它们之间的通信方式,推荐也是variable sharing方式。
相关的对象,会用一些指针把他们相互关联起来,形成一定的数据拓扑。
这通常是数据结构需要解决的问题。
例如形成链式拓扑,树状拓扑,网状拓扑等。
在variable sharing的大环境下,
当需要发生函数调用时,首先需要考虑的问题,就是,数据对象的起点在哪?
对于SOD而言,就是首参,首参一般是一个结构体对象。
对于OOD而言,就是class object,这是默认的首参。
函数调用在执行的过程中,要么会从sharing variable中读取数据到自己的局部变量中,要么会用自己的局部变量中的数据,更新sharing variable。
这些sharing variable,通常都被设计为成员变量。
这样,当另外的函数调用在执行时,只要给定了对应的对象,自然能够找到正确的sharing variable。
也就是说,以同一个对象为首参而发生的不同的函数调用,函数间通信方式,最佳的方式,就是基于成员变量的共享式通信。
++++++++++++++++++++++++++++++++++++++++++++++++++
下面列举几种常见的程序设计框架。
++++++++++++++++++++++++++++++++++++++++
前后台架构
以uart_lite为例,
由于接收过程,是异步的,只能以事件驱动的方式处理,当uart接收到数据后,触发中断作为event,前台callback函数来响应事件。将接收的数据从FIFO中转存到对应的buffer,后台函数在轮询处理时,从buffer中取出数据。
在注册callback时,同时注册了callbackref。
这是callback运行时,所需要的数据环境。
在SOD或者OOD设计时,callbackref通常是一个数据对象,以指针的方式提供。
void uart_lite_isr_handle(struct UartLiteDev* dev)
{
u8 retRegister;
u8 retRegisterVal;
u32 regs = dev->regs;
struct RingBuffer* recv_buff = &dev->recv_buff;
retRegisterVal = XUartLite_GetStatusReg(regs);
XUartLite_WriteReg(regs, XUL_CONTROL_REG_OFFSET, 0);
while (1) {
retRegister = XUartLite_GetStatusReg(regs);
if (retRegister & XUL_SR_RX_FIFO_VALID_DATA) {
ring_buffer_push(recv_buff, XUartLite_ReadReg(regs, XUL_RX_FIFO_OFFSET));
}
else {
break;
}
}
retRegisterVal &= XUL_CR_ENABLE_INTR;
XUartLite_WriteReg(regs, XUL_CONTROL_REG_OFFSET, retRegisterVal);
}
从callbackref中,获取所需要的硬件资源数据,作为后续的硬件操作的依据,
在uart的callback中,读取FIFO中的数据,并转存到ringbuffer中。
这里,调用了ringbuffer的操作集。
在这个架构中,函数间通信,使用的sharing variable,就是callbackref所指定的数据对象。
以及其关联的buffer。
再来看看,对应的后台中,如何在轮询过程中,对buffer进行处理。
首先是轮询的主框架级函数,
SystemManager xsys;
int main()
{
init_platform();
xsys.open_device();
xsys.main_loop();
cleanup_platform();
return 0;
}
这里,定义了全局数据对象xsys,作为系统级的数据对象,
在,main中,inti业务,是由open_device函数来完成的。其中调用了大量的功能级函数实现整板的硬件资源的init和数据对象的inti。
之后,main_loop函数,就是主框架函数了。
void SystemManager::main_loop()
{
boot_state_ = 1;
xout_.set_sdi_reset(1);
watchdog_start();
while(1)
{
watchdog_feed();
xmotor_.recv_loop();
xhost_.recv_loop();
xaec_.compute();
motor_loop();
sensor_loop();
}
}
在这个主框架中,使用了while(1)循环体,实现永续循环,这是轮询处理的主体代码块,
在这个代码块中,调用了多个业务级函数,依次完成业务处理。
来看看recv_loop这个业务级函数的业务逻辑。
void MotorMcu::recv_loop()
{
demux(&uart_->recv_buff, &mcu_cmd_);
mcu2fpga_cmd_parse(&mcu_cmd_);
mcu2fpga_cmd_action(&mcu_cmd_);
}
函数中,调用了三个功能级函数,
demux需要使用ringbuffer,整个关联拓扑是,this对象关联到UART结构体对象,UART结构体对象又关联到ringbuffer结构体对象。demux以this对象作为数据对象起点,可以索引到ringbuffer。
这里,OOD设计的优势得到了体现。合理设计对象之间的关联和拓扑,就能够逻辑清晰地完成数据交易。
demux对ringbuffer进行接收,并将接收的对齐的数据帧,存放到cmd结构体对象中。
之后,调用mcu2fpga_cmd_parse函数,对cmd对象进行解析处理,
cmd对象的设计,使用了数据设计的一种设计思想,即变量化散列。将数据帧中的未符号化(未实义化)的数值,解析后,填充到已经符号化(实义化)的成员变量中。在后续的处理时,直接使用成员变量,可以更逻辑清晰的设计流程代码,使代码可读性更好。
void mcu2fpga_cmd_parse(struct MCU2FPGACommand* mcu_cmd)
{
struct MCU80msCommand* mcu80ms_cmd = &mcu_cmd->mcu80ms_cmd;
struct MCUTestCommand* mcutest_cmd = &mcu_cmd->mcutest_cmd;
struct MCUIrisCommand* mcuiris_cmd = &mcu_cmd->mcuiris_cmd;
mcu80ms_cmd_parse(mcu80ms_cmd);
mcutest_cmd_parse(mcutest_cmd);
mcuiris_cmd_parse(mcuiris_cmd);
}
这个函数,调用了更具体的功能级函数来进行解析。
以其中一个为例,进行说明。
void mcutest_cmd_parse(struct MCUTestCommand* mcutest_cmd)
{
int ret;
if (!mcutest_cmd->fresh)
return;
mcutest_cmd->fresh = 0;
mcutest_cmd->action = 0;
ret = check_xor_crc(&mcutest_cmd->data[2], 6, mcutest_cmd->data[8]);
if (ret < 0)
return;
unsigned char* pdata = (unsigned char*)&mcutest_cmd->data[0];
mcutest_cmd->action = 1;
mcutest_cmd->filter_ok = pdata[7] & 0x1;
mcutest_cmd->iris_ok = (pdata[7] >> 1) & 0x1;
mcutest_cmd->zoom_ok = (pdata[7] >> 2) & 0x1;
mcutest_cmd->focus_ok = (pdata[7] >> 3) & 0x1;
}
可以看出,该函数中,完成了变量化散列的过程,将未实义化的数据帧,实义化为cmc对象中的成员变量的值。
之后,调用mcu2fpga_cmd_action函数。
void MotorMcu::mcu2fpga_cmd_action(struct MCU2FPGACommand* mcu_cmd)
{
struct MCU80msCommand* mcu80ms_cmd = &mcu_cmd->mcu80ms_cmd;
struct MCUTestCommand* mcutest_cmd = &mcu_cmd->mcutest_cmd;
struct MCUIrisCommand* mcuiris_cmd = &mcu_cmd->mcuiris_cmd;
mcu80ms_cmd_action(mcu80ms_cmd);
mcutest_cmd_action(mcutest_cmd);
mcuiris_cmd_action(mcuiris_cmd);
}
这个函数,调用了更具体的功能级函数来进行解析。
以其中一个为例,进行说明。
void MotorMcu::mcutest_cmd_action(struct MCUTestCommand* mcutest_cmd)
{
if (!mcutest_cmd->action)
return;
mcutest_cmd->action = 0;
filter_ok_ = mcutest_cmd->filter_ok;
iris_ok_ = mcutest_cmd->iris_ok;
zoom_ok_ = mcutest_cmd->zoom_ok;
focus_ok_ = mcutest_cmd->focus_ok;
}
可以看出,该函数中,完成了变量化散列的过程,将cmd对象中已经实义化的成员变量值,复制到this对象的对应的成员变量中。
到目前为止,可以看到,业务级函数recv_loop,只是完成了相关数据对象的成员更新,并没有涉及到硬件处理。
void SystemManager::motor_loop()
{
static u32 tv0 = 0;
u32 tv1 = get_timestamp();
u32 dlt_time = ABS_DEC(tv0, tv1);
if (dlt_time < 35) {
return;
}
tv0 = tv1;
switch(zoom_state_)
{
case 1:
{
set_dzoom_scale_inc(0);
if (get_dzoom_scale_state() == 2) {
xmotor_.set_zoom_up(zoom_speed_);
}
}break;
case 2:
{
xmotor_.set_zoom_down(zoom_speed_);
if (xmotor_.get_optics_focus_state() == 2) {
set_dzoom_scale_dec(0);
}
}break;
}
switch(focus_state_)
{
case 1:
{
xmotor_.set_focus_far(focus_speed_);
}break;
case 2:
{
xmotor_.set_focus_near(focus_speed_);
}break;
default:
break;
}
}
在motor_loop中,根据数据对象中设置的flag,执行实际的动作。
以其中一个为例。
void MotorMcu::set_zoom_up(int speed)
{
struct FPGA2MCUCommand cmd;
fpga2mcu_cmd_init(&cmd);
fpga2mcu_set_zoom_field(&cmd, 1, (u8)speed);
fpga2mcu_cmd_calc_crc(&cmd);
uart_lite_send(uart_, cmd.data, cmd.len);
}
该函数,构造具体的cmd结构体对象后,填充对应的成员变量, 之后,就通过UART发送出去了。
variable sharing的函数间通信方式,
贯穿了整个架构。
如果系统中需要添加其他的功能业务,那么只需要按照相似的框架,完成数据设计和处理设计,即可方便的实现。
每一个业务模块,使用自己的业务数据对象,在业务处理的流程中,各个函数,从sharing variable中读取所需的数据,处理后,再将自己的处理结果,写入sharing variable中,供后续的函数使用。