驱动程序和C功能块
目标不仅负责启动生成的模型,还负责访问目标平台的功能:GPIO,UART,ADC,PWM,定时器,总线和外部芯片。 通常,这种访问是通过C-驱动程序组织的,这些驱动程序被复制到目标项目中,并从模型的C-代码中调用。
从模型中调用C代码的主要用户机制是一个块 C Function. 因此,在设计目标时,提前确定向用户提供了哪些API以及此API将如何进入生成的项目非常重要。
什么是C函数?
座 C Function*允许您在模型内部使用C代码 *AnyMath . 用户调整输入、输出、参数和操作变量,并在块编辑器中编写代码。
来自编辑 C Function 主要有四个部分:
-
Output code-在块计算的每一步执行的代码; -
Start code-初始化时执行一次的代码; -
Terminate code-在停止期间执行一次的代码; -
Shared code-多个实例的共享代码、函数和全局数据 C Function 具有相同的函数名称。
在生成C代码时,这些部分的内容被转换成包含在生成的模型代码中的C函数。 这意味着块中的代码是 C Function 它与目标运行时绑定一起编译,如果标头和驱动程序源添加到项目中,则可以从驱动程序调用函数。
编辑 C Function 显示当前输入、输出、参数和操作变量的原型函数。 对于目标,重要的是驱动程序文档允许用户在生成的接口函数中唯一标识每个驱动程序调用的位置。 — 初始化, 步骤 和 任期 —无需独立分析或假设。
操作变量 C Function 用于块的特定实例的状态。 它们很方便地用于缓冲区、计数器、描述符和其他不应该对整个模型是全局的数据。 如果状态应该由具有相同函数名的块的多个实例共享,则将其放置在节中 Shared code.
对于一个正常的模拟 C Function 可以与C API一起使用共享库。 为了以独立执行为目标,更经常使用另一种选择:将必要的C代码和驱动程序添加到项目中,并与目标平台的模型一起编译。
为什么target需要自己的驱动程序?
座 C Function 它可以包含任意的C代码,但模型的用户不必每次都编写寄存器,HAL或SDK的低级工作的代码。 目标可以提供稳定的驱动器层。:
*用于自定义块的简单Si API;
*带有常量和类型的头文件;
*特定于平台的实现;
*块模板 .nglib,生成对此API的调用。
该层将模型与硬件部分分开。 用户调用,例如, digitalWrite(。..) 或 uart_transmit(。..) 目标决定后面是哪些寄存器、HAL函数或SDK调用。
驱动程序如何进入该项目?
驱动程序通常位于目录中的目标旁边 司机. 在 generate_executable_code target将这些文件复制到项目目录,构建系统将它们包含在编译中。
将驱动程序复制到项目的示例
drivers_files = _get_drivers_src_list()
for file in drivers_files:
shutil.copy2(file, project_src)
如果用户通过添加他们的源文件 C Function ,目标可以通过保存它们 store_filed_sources(model_settings。cmi,project_src). 当模型包含其他文件时,这很有用。 .c/.h 这不是目标本身的一部分。
从C函数保存用户文件的示例
store_model_src(model_code.c_code, project_src, codegen_info)
store_filed_sources(model_settings.cmi, project_src)
装配系统应该同时看到生成的模型文件和驱动程序。 在提供的 示例在CMake模板 自动柜员机328 这是通过 格洛布 对于目录 司机 和项目文件。
在CMake中启用驱动程序的示例
file(
GLOB MODEL_SOURCES
"${MODELS_PATH_DIR}/drivers/*.c"
"${MODELS_PATH_DIR}/drivers/SWSerial/*.c"
"${MODELS_PATH_DIR}/*.c"
)
add_executable(${TARGET}_elf ${MODEL_SOURCES})
ArduinoUNO的例子:Serial as A Si API
在提供的 例子在 工程师-支援-包/目标/苦心经营/驱动 在Arduino的顶部有一个基本的UART驱动程序 系列产品. 标题 串行。h 声明与C兼容的API,以及实现 串行。cpp 调用Arduino C++API。
这种方法很重要:来自块的代码 C Function 并且模型生成的C代码可以调用简单的C函数,驱动内部可以使用C++平台对象。
片段示例 串行。h
#ifdef __cplusplus
extern "C" {
#endif
void beginSerial(uint8_t serial_port, uint32_t baudrate, int config);
void endSerial(uint8_t serial_port);
bool isReadySerial(uint8_t serial_port);
uint8_t availableSerial(uint8_t serial_port);
size_t readBytesSerial(uint8_t serial_port, uint8_t *buffer, int len);
size_t writeSerial(uint8_t serial_port, uint8_t *buffer, int len);
#ifdef __cplusplus
}
#endif
在驱动程序内部使用Arduino API的示例
void beginSerial(uint8_t serial_port, uint32_t baudrate, int config)
{
if (config == UNSPEC) config = SERIAL_8N1;
Serials[serial_port]->begin(baudrate, config);
}
size_t writeSerial(uint8_t serial_port, uint8_t *buffer, int len)
{
return Serials[serial_port]->write(buffer, len);
}
从街区 C Function 用户可以在 Start code, Output code 和 Terminate code.
C函数中的用法示例
/* StartCode */
beginSerial(0, 115200, SERIAL_8N1);
/* OutputCode */
uint8_t value = (uint8_t)input1;
writeSerial(0, &value;, 1);
/* TerminateCode */
endSerial(0);
ATmega328示例:GPIO,ADC,UART和定时器
在提供的 例子在 工程师支持-包/目标/ATmega328/驱动程序 驱动程序更接近硬件:它们直接与AVR寄存器一起工作。 有:
-
gpio。h/gpio。c-数字I/O,ADC,PWM; -
uart。h/uart。c-接收和发送具有错误状态的UART; -
计时器。h/计时器。c-设置定时器; -
中断。h/中断。c-中断的辅助功能; -
位。h-用于处理位和寄存器的宏; -
[医]SWSerial-程序化系列产品.
在 gpio。h API由一组简单的函数和常量表示,这些函数和常量可能导致 C Function 或生成的硬件单元。
GPIO API片段示例
#define HIGH 0x01
#define LOW 0x00
#define OUT 0x01
#define IN 0x00
void pinMode(int reg, int pin, int out);
void digitalWrite(int port, int bit, uint8_t high);
int digitalRead(int pin, int bit);
int analogRead(int channel);
void adcInit(int prescaler);
void analogWrite(int port, int bit, int duty, int inverse);
ATmega328UART驱动程序显示了另一个有用的技巧:函数返回具有操作状态的结构。 总比把它还给我好。 0/1 因为模型可以区分时间限制,帧错误,奇偶校验错误,缺乏初始化和部分接收。
UART操作结果示例
typedef struct {
uint8_t bytes_transmitted;
uart_status_t status;
} uart_transmit_result_t;
uart_transmit_result_t uart_transmit(
const unsigned char *data,
const unsigned char length,
uint16_t timeout_ms,
uint8_t disable_interrupts
);
自定义块库

驱动程序和C API本身不提供用户友好的模拟界面。 这样用户就可以像常规块一样添加硬件定位功能。 AnyMath ,他们需要组织成一个自定义库。
用户库 AnyMath 是文件 .nglib,其中包含块或子系统并显示在块库中 AnyMath . 该机制在文章中描述。 工程师用户图书馆.
为目标 .nglib 它通常用作支持包的公共部分。:
用户从库中选择一个块,而不是编写代码 *C Function 手动; *块掩码设置明确的参数:端口,引脚,UART速度,ADC通道,PWM频率; *块内的代码调用目标驱动程序的Si API; *相同的块可用于不同的模型。; *库可以提供支持包和示例。
如果块很少,一个文件就足够了。 .nglib 例如 我的冲浪板。nglib. 如果目标包含许多块,最好按主题划分它们。: [医]GPIO, UART, 艺发局, 脉宽调制, 计时器, 通讯. 您可以为此结构使用文件。 engee_library。汤姆尔,其中描述了哪些文件 .nglib 以及应该包括块库的哪些部分。
多级目标库示例
[metadata]
format_version = "1"
[[categories]]
lib_path = "/MyBoard/GPIO"
files = ["gpio/digital.nglib", "gpio/analog.nglib"]
[[categories]]
lib_path="/MyBoard/Communication/UART"
文件=["通信/uart。nglib"]
[[categories]]
lib_path="/MyBoard/计时器"
files=["定时器/定时器。nglib"]
文件本身 .nglib 它可以存储在驱动程序、模板和演示模型旁边的目标结构中。 重要的是,在下载支持包后,用户可以将库目录添加到路径中。 AnyMath 并查看用户库部分中的块。
实际上,这意味着目标文档不仅要描述驱动程序的C函数,还要描述相应的块。:
*它在哪里 .nglib;
*库的哪个部分将包含块;
*面具有哪些参数?;
*内部调用哪个Si API;
*块的限制是什么:允许的端口,频率,缓冲区大小,计算步骤;
*该装置是否只在独立模式下工作,或也支持主机上的模拟。
C函数与库块的关系
目标可能仅限于文档 C Function ,但通常更方便的是给用户现成的硬件块在 .nglib. 这样的块里面可以建立在 C Function 或另一种代码生成机制,但其任务是相同的:将c API隐藏在明确的块掩码后面。
例如,代替用户手动写:
pinMode(DDRB_REG, 5, OUT);
digitalWrite(PORTB_REG, 5, HIGH);
您可以创建一个*数字写入*块,用户在其中选择掩码中的端口,引脚和初始状态。 块代码将生成驱动程序调用本身。
重要的是,块掩码的参数直接对应于C API的参数或唯一地转换为它们。 如果掩码使用直观的值,如 PB5, 输出, 引体向上,然后块必须将它们转换为驱动程序常量。
构建参数和其他源
在 C Function 有构建选项和使用自定义源文件的能力。 对于target,这意味着在生成项目时必须保存所有额外的源代码文件(.c/.h)和包含头文件的路径。
在 恩吉。综合服务 这有两种机制。:
-
store_filed_资源从块保存用户源文件 C Function ; -
CMIParser._parse_cfunction_comments它可以从特殊注释中提取额外的编译参数。 C Function .
如果目标支持块中的自定义依赖项 C Function ,你需要明确决定:
*允许添加哪些源文件; *它们在项目中复制到哪里; *将哪些可插入目录添加到构建中; *是否允许使用外部库?; *用户如何看到构建错误。
对于目标的初始版本,您只能支持与模型一起编译的C代码,并在以后添加外部库。
设计C API驱动程序的建议
目标驱动程序成为模型的公共API,因此它们的设计应该考虑到长期兼容性。:
*使用简单的C类型: uint8_t, uint16_t, uint32_t, 浮子,浮子, 双倍 和指向缓冲区的指针;
*避免在模型步骤中使用需要动态内存分配的API。;
*明确记录测量单位:微秒、毫秒、赫兹、比特/秒、PWM百分比;
*如果操作可能失败,则返回错误状态;
*不要长时间阻塞调度程序:长时间的UART接收或等待ADC完成可能导致违反TET;
*单独初始化、步骤工作和资源释放;
*考虑一下 Start code 叫过一次, Output code -在每一步, Terminate code -停车时;
*使用 extern"C" 在头文件中,如果驱动程序实现是用C++编写的;
*明确指定从中断处理程序(ISR)或RTOS任务调用哪些函数是安全的。