Metal Bare嵌入式软件架构小谈
软硬件模型
不管是通信互联系统,图形图像,音频视频,一个满足某种需求的业务应用,都常常需要协同使用硬件和软件来配合完成。硬件快,天然的具备处理数字的或模拟的信号的能力;软件灵活,可配置可定制可更新。那些固定的算法或已经成为业界标准的成熟规格,不像面向用户的使用场景一样会频繁修改,但对性能指标有高要求,比如MPEG编解码,颜色空间转换,还有那些离不开硬件实现的协议物理层(比如有对模拟信号的快速处理),你很难想象现在手机里是软件在做视频流解码,OPENGL渲染或者wifi的物理层信道编码调制等,这些功能单一的工作就该交给硬件(前面和后面所指的硬件基本都是指ASIC)来实现。而软件可以用来处理业务复杂但对性能没有苛刻要求的工作,相对于前面的协议物理层,不再需要同模拟信号和高速数字信号编解码打交道的协议链路层其实已经可以交给软件来做了,当然交给硬件做更好,更不用说更往上的应用层。站在把硬件作为被不同应用独占的资源的角度上来看待,软件扮演的是资源分配的仲裁者,根据业务需求来把硬件分配到应用上。站在系统中数据的流动的角度来看,有些硬件单元是外界数据的消费者,有些硬件单元是数据的生产者,有些既是生产者也是消费者,数据本身只在硬件和硬件单元之间流动,它们就像铁轨或航线,承载着作为数据的火车或飞机,而软件就像一个铁路调度器或者空中管制者,根据应用需要,把不同的数据消费者和生产者连接起来。这其实和Unix系统的基于管道架构的过滤器的思想很像,比如当你在shell上执行:
ls *.c | xargs cat | grep "stdio"
操作系统管道就把ls, xargs, cat, grep几个程序连接起来,ls, cat和grep们就像是具备单一功能干具体活儿的硬件模块,它们通过定义好的接口即Unix上的标准输入和标准输出消费和生产数据(硬件通过定义良好的接口即寄存器或配置自己的参数行为,如上面作为程序参数的字符串”*.c”和‘“stdio”,或设置存储单元地址接收输入数据或存放输出数据,如标准输入输出缓冲),软件同样扮演操作系统的角色,它创建/销毁相应的进程(enable/disable相应的硬件模块),重定向进程输入输出(决定硬件工作的时间点)。标题里的Mental Bare主要指软件所在的处理器环境很简单,不需要使用操作系统的,比如没有MMU,没有多核,8位或者16位的简单MPU。虽然软件的runtime比较简单,但还是有很多需要“架构”的地方。
最简单的模式:阻塞式super loop
在分析一个嵌入式系统需求的时候,就要搞清楚系统的数据流是什么,哪些是生产者,它们或许是来自外界的输入数据(比如UART的FIFO IN DATA),或者是反映状态的变化(比如DMA buffer满了?BlutTooth PHY Transimitter空闲了,包传输结束了?),哪些是消费者(比如为一个buffer请求一个新的DMA write操作,为一个包请求一次新的BT PHY Transimitter 事务)。它们之间的”连接点”,即需要软件插手的地方是什么。一个这样的系统,如果够简单的话,总是可以直接实现成最简单的super loop的方式:
void main(void)
{
while(1) {
hw_poll_input();
sw_process_data();
hw_output();
}
}
上面程序三行代码分别代表查询input I/O有没有新的数据产生–>处理数据(如果需要的话)–>通知output I/O去消费数据。
如果系统的行为能用一个状态机来建模或流程图来表达的话,都可以实现成这种super loop。这里的变化无非就是作为模型的状态机可能会很复杂,比如software同input I/O和output I/O有复杂的协议和握手过程,增加的复杂性也是在这个最简单的super loop的骨架上完善其血肉。
非阻塞式状态机模式
这种代码是顺序且阻塞式的,比如在hw_poll_input()中,可能会有这样的代码:
void hw_poll_input()
{
while (!(read_reg(STATUS_REG) & BIT_DATA_ARRIVALLING));
...
}
因为阻塞,所以当系统无法简单地建模成一个状态机,而是存在多个并行的状态机时,就不能这样做了,(这里的状态机,其实也就是任务/task),因为程序会阻塞在其中一个任务里,无法响应另一个任务的随机事件。想象一下,如果一个web服务器只能一个一个的接受请求会怎么样?
非阻塞模式就是把每个任务的程序都拆分为非顺序执行的一段一段的有限状态机(FSM)形式,比如上面那个程序就写成
void statemachine()
{
switch(state)
{
case WAIT_INPUT_READY:
if(!(read_reg(STATUS_REG) & BIT_DATA_ARRIVALLING)) {
return;
} else {
write_reg(STATUS_REG,BIT_DATA_ARRIVALLING);
sw_process_data();
state = WAIT_OUTPUT_READY;
}
break;
case WAIT_OUTPUT_READY:
if (!(read_reg(STATUS_REG2)&BIT_OUT_FIFO_EMPTY)) {
return;
}
else {
write_reg(STATUS_REG2, BIT_OUT_FIFO_EMPTY);
write_reg(FIFO_DAT, value);
state = WAIT_INPUT_READY;
break;
}
}
}
两个状态WAIT_INPUT_READY和WAIT_OUTPUT_READY分别对应上面那个等价的顺序执行的程序的两个阻塞点。于是当“阻塞”发生时,就从函数返回。
注意上面的代码在每次查询到状态寄存器有事件(从0变到1)后,都有一句
write_reg(STATUS_REG,BIT_XXX);
去清除它。这是一种最常见的软件/硬件交互的协议,硬件在状态寄存器记录事件发生,软件查询状态寄存器发现事件发生,最后软件向硬件表明”我已经知道了”,硬件看到软件已经知道事件发生了,就可以接下来继续接收新的事件了。
这样,两个并行的状态机就可以”并行”运行了,任意一个被阻塞都不会影响到另一个,例如:
void main(void)
{
while(1) {
statemachine_a(); //process A
statemachine_b(); //process B
}
}
异步I/O模式
上面的代码都是同步I/O模式,即程序主动去询问I/O”事件发生了吗?”,这样的好处是程序的执行符合人的思维模式,同步的执行不会发生竞态。但坏处是很多cpu cycle被浪费在无谓的查询上。比如并行的状态机a,b,c,d….越来越多的时候,每次main loop都要查询一遍。任务在每个状态时需要查询的事件变得越来越多时,例如:
case WAIT_INPUT_READY:
if(!(read_reg(STATUS_REG) & BIT_EVENT1_ARRIVALLING) && !(read_reg(STATUS_REG) & BIT_EVENT2_ARRIVING) && !(read_reg(STATUS_REG) & BIT_EVENT3_ARRIVING) && timeout(TIMEOUT_500MS)) {
return;
} else {
}
这里只要event1, event2, event3和time out发生都可以触发到下一个状态,对cpu cycle的浪费也越严重(大部分时候都是在执行无意义的代码)。
异步I/O处理模式可以把主动的”轮询”变为被动的”按需调度”。异步I/O需要利用cpu的异步执行代码的能力,如中断,定时器。但同时异步执行的代码也是危险的,就像多线程引入了程序的不确定性一样,被中断的代码和中断中执行的代码如果访问到同样的资源(memory地址,硬件寄存器),都有可能造成这些资源被非原子性地读写而出现很难发现的错误。其次,在中断处理程序里最好不要做太多的事,最好只是简单处理一下现场,记录一下状态就返回,真正的干活儿还是交给工作任务来做,这里使用消息队列的模式来解耦中断处理程序和工作任务程序是一个很好的方法。
每个状态机执行代码仍然在main loop中执行,不过,它们不再主动的查询I/O事件,而是被动的被一个消息队列调度者来调用它们。状态机函数从主动查询的“拉”模式变为了被动调用的“推”模式。
void statemachine(msg_t Msg)
{
switch(state)
{
case WAIT_INPUT_READY:
if (msg_is(Msg, EVENT1_ARRIVING) || msg_is(Msg, EVENT2_ARRIVING) || msg_is(Msg, EVENT3_ARRIVING) || !msg_is(Msg, TIMEOUT_500MS)) {
sw_process_data();
state = WAIT_OUTPUT_READY;
}
else {
return;
}
}
}
根级的main函数可以就是那个消息队列调度者:
void main(void)
{
while(1) {
if (!msg_queue_empty()) {
msg_t Next_Msg = msg_queue_pop();
statemachine_a(Next_Msg);
statemachine_b(Next_Msg);
}
}
}
硬件设计时,最好让每当STATUS_REG的每个bit发生从0到1的状态变化时都有中断产生,那么,查询STATUS_REG状态的工作就放在中断函数中执行:
void IRQ(void)
{
if (read_reg(STATUS_REG) & BIT_EVENT1_ARRIVING) {
write_reg(STATUS_REG, BIT_EVENT1_ARRIVING);
msg_queue_push(new_msg(EVENT1_ARRIVING));
}
if (read_reg(STATUS_REG) & BIT_EVENT2_ARRIVING) {
write_reg(STATUS_REG, BIT_EVENT2_ARRIVING);
msg_queue_push(new_msg(EVENT2_ARRIVING));
}
if (read_reg(STATUS_REG) & BIT_EVENT3_ARRIVING) {
write_reg(STATUS_REG, BIT_EVENT3_ARRIVING);
msg_queue_push(new_msg(EVENT3_ARRIVING));
}
}
这里也是有竞态的,这里IRQ函数和main函数共享了对消息队列的操作,所以,应该在消息队列操作msg_queue_push和msg_queue_pop函数中使用disable IRQ等方法来保护队列指针不被破坏。
异步程序容易出错,特别是程序本身逻辑具备同步性的时候。
比如下面一个例子:

本文探讨了Metal Bare嵌入式软件架构中的软硬件模型,从阻塞式super loop、非阻塞式状态机模式到异步I/O模式,阐述了如何在简单处理器环境中构建高效软件架构。通过状态机和协程的概念,展示了如何处理并发和事件驱动的挑战。
最低0.47元/天 解锁文章
539

被折叠的 条评论
为什么被折叠?



